From 35118ab2d975d6bb3cf03789d34a7c090eb1e790 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?= <76204082+iamsedatozturk@users.noreply.github.com> Date: Tue, 6 Oct 2026 14:24:30 +0300 Subject: [PATCH] =?UTF-8?q?Postggre=20g=C3=BCncellemeleri=20ve=20colorbox?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/instructions/ai.instructions.md | 6 +- .github/instructions/dotnet.instructions.md | 8 + .github/instructions/lowcode.instructions.md | 55 +- README.md | 13 +- .../McpErrorCodes.cs | 1 - .../Services/McpTableCreator.cs | 44 +- .../SqlObjectManagerAppService.cs | 53 +- .../Migrations/WizardDataSeeder.cs | 19 +- .../PlatformDbMigratorModule.cs | 23 +- .../Data/ListFormSeedDataApplier.cs | 12 +- .../Data/PostgreSqlTableScript.cs | 366 +++++ .../Queries/DefaultValueManager.cs | 4 +- .../Queries/QueryManager.cs | 74 - .../DynamicData/SpRepository.cs | 45 +- .../EntityFrameworkCore/PlatformDbContext.cs | 54 +- .../PostgreSqlModelSyntax.cs | 70 + .../DbStartup/SetupMigrationRunner.cs | 294 +++- .../PredefinedReports/DynamicFormReport.cs | 30 +- .../DynamicReportColorHelper.cs | 173 +++ .../DynamicReportLayoutHelper.cs | 27 +- .../DynamicReportSqlBuilder.cs | 94 +- claude.md | 20 +- .../data/App.Languages.LanguageText.json | 14 - ...ieldsDictionaryJson_SaveDbTableColumns.sql | 271 ++-- .../object/Sas_H_BackgroundWorker_JobFlow.sql | 73 +- .../seeds/postgres/object/Adm_T_Contact.sql | 59 + .../object/Adm_T_ContactBankAccount.sql | 19 + .../postgres/object/Adm_T_ContactPerson.sql | 21 + .../postgres/object/Adm_T_ContactTag.sql | 16 + .../seeds/postgres/object/Hr_F_HrSetting.sql | 25 + .../postgres/object/Hr_F_LeaveNetDays.sql | 37 + .../postgres/object/Hr_F_LeaveWorkDays.sql | 76 + .../object/Hr_F_TimesheetPeriodClosed.sql | 26 + .../object/Hr_F_WorkCalendarDayType.sql | 28 + .../postgres/object/Hr_S_AttendanceImport.sql | 125 ++ .../object/Hr_S_AttendanceProcess.sql | 97 ++ .../object/Hr_S_DevelopmentPlanSync.sql | 17 + .../postgres/object/Hr_S_LeaveDaysRefresh.sql | 26 + .../postgres/object/Hr_S_LeaveNotify.sql | 44 + .../object/Hr_S_LeaveRequestAfterSave.sql | 393 ++++++ .../object/Hr_S_LeaveRequestGuard.sql | 72 + .../object/Hr_S_LeaveSickLeaveSync.sql | 106 ++ .../object/Hr_S_PerformanceFeedbackOpen.sql | 31 + .../postgres/object/Hr_S_PerformanceGuard.sql | 66 + .../object/Hr_S_PerformanceNotify.sql | 42 + .../object/Hr_S_PerformancePeriodActivate.sql | 54 + .../Hr_S_PerformanceReviewAfterSave.sql | 117 ++ .../object/Hr_S_PerformanceReviewRecalc.sql | 86 ++ .../object/Hr_S_TimesheetCalculate.sql | 461 ++++++ .../postgres/object/Hr_S_TimesheetNightly.sql | 70 + .../object/Hr_S_TimesheetPeriodAfterSave.sql | 68 + .../postgres/object/Hr_S_TrainingSync.sql | 41 + .../object/Hr_S_WorkCalendarAfterSave.sql | 55 + .../postgres/object/Hr_T_AttendanceLog.sql | 27 + .../postgres/object/Hr_T_BenefitType.sql | 20 + .../seeds/postgres/object/Hr_T_Candidate.sql | 53 + .../postgres/object/Hr_T_CandidateSource.sql | 16 + .../postgres/object/Hr_T_Certificate.sql | 15 + .../postgres/object/Hr_T_DevelopmentPlan.sql | 64 + .../postgres/object/Hr_T_DisciplinaryCase.sql | 25 + .../object/Hr_T_DisciplinaryDecision.sql | 21 + .../object/Hr_T_DisciplinaryDefense.sql | 19 + .../postgres/object/Hr_T_DisciplinaryFile.sql | 18 + .../object/Hr_T_DisciplinaryPenaltyType.sql | 17 + configs/seeds/postgres/object/Hr_T_Doctor.sql | 20 + .../postgres/object/Hr_T_DocumentType.sql | 17 + configs/seeds/postgres/object/Hr_T_Driver.sql | 20 + .../postgres/object/Hr_T_DriverAccident.sql | 19 + .../postgres/object/Hr_T_DriverDocument.sql | 17 + .../postgres/object/Hr_T_DriverPenalty.sql | 20 + .../object/Hr_T_EvaluationCriterion.sql | 17 + .../seeds/postgres/object/Hr_T_EventLog.sql | 21 + .../postgres/object/Hr_T_ExpenseType.sql | 21 + .../seeds/postgres/object/Hr_T_Hospital.sql | 15 + .../object/Hr_T_HospitalDepartment.sql | 15 + .../seeds/postgres/object/Hr_T_Interview.sql | 26 + .../postgres/object/Hr_T_InterviewScore.sql | 28 + .../postgres/object/Hr_T_JobApplication.sql | 29 + .../seeds/postgres/object/Hr_T_JobOffer.sql | 43 + .../seeds/postgres/object/Hr_T_JobPosting.sql | 30 + .../object/Hr_T_LeaveBalanceAdjustment.sql | 28 + .../postgres/object/Hr_T_LeaveRequest.sql | 58 + .../seeds/postgres/object/Hr_T_LeaveType.sql | 31 + .../object/Hr_T_PerformanceCompetency.sql | 24 + .../object/Hr_T_PerformanceFeedback.sql | 41 + .../object/Hr_T_PerformanceFeedbackScore.sql | 20 + .../postgres/object/Hr_T_PerformanceGoal.sql | 60 + .../object/Hr_T_PerformancePeriod.sql | 48 + .../object/Hr_T_PerformancePeriodSkill.sql | 19 + .../object/Hr_T_PerformanceRating.sql | 39 + .../object/Hr_T_PerformanceReview.sql | 54 + .../postgres/object/Hr_T_RejectionReason.sql | 17 + configs/seeds/postgres/object/Hr_T_Shift.sql | 22 + .../postgres/object/Hr_T_StaffRequest.sql | 34 + .../seeds/postgres/object/Hr_T_Timesheet.sql | 39 + .../postgres/object/Hr_T_TimesheetPeriod.sql | 21 + .../seeds/postgres/object/Hr_T_Training.sql | 35 + .../postgres/object/Hr_T_TrainingFile.sql | 18 + .../object/Hr_T_TrainingParticipant.sql | 23 + .../postgres/object/Hr_T_TrainingSubject.sql | 17 + .../postgres/object/Hr_T_TrainingType.sql | 15 + .../seeds/postgres/object/Hr_T_Vehicle.sql | 32 + .../object/Hr_T_VehicleDestination.sql | 16 + .../postgres/object/Hr_T_VehicleDocument.sql | 17 + .../postgres/object/Hr_T_VehicleExpense.sql | 20 + .../object/Hr_T_VehicleExpenseDefinition.sql | 16 + .../object/Hr_T_VehicleExpenseLine.sql | 21 + .../postgres/object/Hr_T_VehicleFile.sql | 17 + .../postgres/object/Hr_T_VehicleMovement.sql | 34 + .../seeds/postgres/object/Hr_T_Visitor.sql | 23 + .../postgres/object/Hr_T_WorkCalendar.sql | 20 + .../postgres/object/Hr_V_AttendanceShift.sql | 39 + .../postgres/object/Hr_V_LeaveBalanceYear.sql | 124 ++ .../postgres/object/Hr_V_LeaveSummary.sql | 32 + .../postgres/object/Hr_V_TimesheetMonthly.sql | 60 + .../postgres/object/Prc_T_DeliveryMethod.sql | 15 + .../seeds/postgres/object/Prc_T_Incoterm.sql | 16 + .../postgres/object/Prc_T_PaymentMethod.sql | 16 + .../postgres/object/Prc_T_PaymentTerm.sql | 17 + .../seeds/postgres/object/Prj_T_Customer.sql | 18 - .../seeds/postgres/object/Prj_T_Milestone.sql | 17 - configs/seeds/postgres/object/Prj_T_Tag.sql | 16 - configs/seeds/postgres/object/Prj_T_Task.sql | 30 - .../postgres/object/Prj_T_TaskDependency.sql | 17 - .../seeds/postgres/object/Prj_T_Timesheet.sql | 19 - .../Sas_H_ListFormFieldsDictionaryJson.sql | 1250 +++++------------ .../object/Sas_H_OrderItem_ByTenantId.sql | 11 +- .../Sas_H_ListFormFieldsDictionaryJson.sql | 385 +++-- configs/seeds/wizard/Contact.json | 8 +- .../form/editors/TagBoxEditorComponent.tsx | 6 + ui/src/views/list/GridColumnData.ts | 2 + .../list/editors/TagBoxEditorComponent.tsx | 2 + ui/src/views/list/useListFormColumns.ts | 84 +- ui/src/views/shared/lookupColor.tsx | 60 + 134 files changed, 6203 insertions(+), 1694 deletions(-) create mode 100644 api/src/Sozsoft.Platform.Domain/Data/PostgreSqlTableScript.cs create mode 100644 api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PostgreSqlModelSyntax.cs create mode 100644 api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportColorHelper.cs create mode 100644 configs/seeds/postgres/object/Adm_T_Contact.sql create mode 100644 configs/seeds/postgres/object/Adm_T_ContactBankAccount.sql create mode 100644 configs/seeds/postgres/object/Adm_T_ContactPerson.sql create mode 100644 configs/seeds/postgres/object/Adm_T_ContactTag.sql create mode 100644 configs/seeds/postgres/object/Hr_F_HrSetting.sql create mode 100644 configs/seeds/postgres/object/Hr_F_LeaveNetDays.sql create mode 100644 configs/seeds/postgres/object/Hr_F_LeaveWorkDays.sql create mode 100644 configs/seeds/postgres/object/Hr_F_TimesheetPeriodClosed.sql create mode 100644 configs/seeds/postgres/object/Hr_F_WorkCalendarDayType.sql create mode 100644 configs/seeds/postgres/object/Hr_S_AttendanceImport.sql create mode 100644 configs/seeds/postgres/object/Hr_S_AttendanceProcess.sql create mode 100644 configs/seeds/postgres/object/Hr_S_DevelopmentPlanSync.sql create mode 100644 configs/seeds/postgres/object/Hr_S_LeaveDaysRefresh.sql create mode 100644 configs/seeds/postgres/object/Hr_S_LeaveNotify.sql create mode 100644 configs/seeds/postgres/object/Hr_S_LeaveRequestAfterSave.sql create mode 100644 configs/seeds/postgres/object/Hr_S_LeaveRequestGuard.sql create mode 100644 configs/seeds/postgres/object/Hr_S_LeaveSickLeaveSync.sql create mode 100644 configs/seeds/postgres/object/Hr_S_PerformanceFeedbackOpen.sql create mode 100644 configs/seeds/postgres/object/Hr_S_PerformanceGuard.sql create mode 100644 configs/seeds/postgres/object/Hr_S_PerformanceNotify.sql create mode 100644 configs/seeds/postgres/object/Hr_S_PerformancePeriodActivate.sql create mode 100644 configs/seeds/postgres/object/Hr_S_PerformanceReviewAfterSave.sql create mode 100644 configs/seeds/postgres/object/Hr_S_PerformanceReviewRecalc.sql create mode 100644 configs/seeds/postgres/object/Hr_S_TimesheetCalculate.sql create mode 100644 configs/seeds/postgres/object/Hr_S_TimesheetNightly.sql create mode 100644 configs/seeds/postgres/object/Hr_S_TimesheetPeriodAfterSave.sql create mode 100644 configs/seeds/postgres/object/Hr_S_TrainingSync.sql create mode 100644 configs/seeds/postgres/object/Hr_S_WorkCalendarAfterSave.sql create mode 100644 configs/seeds/postgres/object/Hr_T_AttendanceLog.sql create mode 100644 configs/seeds/postgres/object/Hr_T_BenefitType.sql create mode 100644 configs/seeds/postgres/object/Hr_T_Candidate.sql create mode 100644 configs/seeds/postgres/object/Hr_T_CandidateSource.sql create mode 100644 configs/seeds/postgres/object/Hr_T_Certificate.sql create mode 100644 configs/seeds/postgres/object/Hr_T_DevelopmentPlan.sql create mode 100644 configs/seeds/postgres/object/Hr_T_DisciplinaryCase.sql create mode 100644 configs/seeds/postgres/object/Hr_T_DisciplinaryDecision.sql create mode 100644 configs/seeds/postgres/object/Hr_T_DisciplinaryDefense.sql create mode 100644 configs/seeds/postgres/object/Hr_T_DisciplinaryFile.sql create mode 100644 configs/seeds/postgres/object/Hr_T_DisciplinaryPenaltyType.sql create mode 100644 configs/seeds/postgres/object/Hr_T_Doctor.sql create mode 100644 configs/seeds/postgres/object/Hr_T_DocumentType.sql create mode 100644 configs/seeds/postgres/object/Hr_T_Driver.sql create mode 100644 configs/seeds/postgres/object/Hr_T_DriverAccident.sql create mode 100644 configs/seeds/postgres/object/Hr_T_DriverDocument.sql create mode 100644 configs/seeds/postgres/object/Hr_T_DriverPenalty.sql create mode 100644 configs/seeds/postgres/object/Hr_T_EvaluationCriterion.sql create mode 100644 configs/seeds/postgres/object/Hr_T_EventLog.sql create mode 100644 configs/seeds/postgres/object/Hr_T_ExpenseType.sql create mode 100644 configs/seeds/postgres/object/Hr_T_Hospital.sql create mode 100644 configs/seeds/postgres/object/Hr_T_HospitalDepartment.sql create mode 100644 configs/seeds/postgres/object/Hr_T_Interview.sql create mode 100644 configs/seeds/postgres/object/Hr_T_InterviewScore.sql create mode 100644 configs/seeds/postgres/object/Hr_T_JobApplication.sql create mode 100644 configs/seeds/postgres/object/Hr_T_JobOffer.sql create mode 100644 configs/seeds/postgres/object/Hr_T_JobPosting.sql create mode 100644 configs/seeds/postgres/object/Hr_T_LeaveBalanceAdjustment.sql create mode 100644 configs/seeds/postgres/object/Hr_T_LeaveRequest.sql create mode 100644 configs/seeds/postgres/object/Hr_T_LeaveType.sql create mode 100644 configs/seeds/postgres/object/Hr_T_PerformanceCompetency.sql create mode 100644 configs/seeds/postgres/object/Hr_T_PerformanceFeedback.sql create mode 100644 configs/seeds/postgres/object/Hr_T_PerformanceFeedbackScore.sql create mode 100644 configs/seeds/postgres/object/Hr_T_PerformanceGoal.sql create mode 100644 configs/seeds/postgres/object/Hr_T_PerformancePeriod.sql create mode 100644 configs/seeds/postgres/object/Hr_T_PerformancePeriodSkill.sql create mode 100644 configs/seeds/postgres/object/Hr_T_PerformanceRating.sql create mode 100644 configs/seeds/postgres/object/Hr_T_PerformanceReview.sql create mode 100644 configs/seeds/postgres/object/Hr_T_RejectionReason.sql create mode 100644 configs/seeds/postgres/object/Hr_T_Shift.sql create mode 100644 configs/seeds/postgres/object/Hr_T_StaffRequest.sql create mode 100644 configs/seeds/postgres/object/Hr_T_Timesheet.sql create mode 100644 configs/seeds/postgres/object/Hr_T_TimesheetPeriod.sql create mode 100644 configs/seeds/postgres/object/Hr_T_Training.sql create mode 100644 configs/seeds/postgres/object/Hr_T_TrainingFile.sql create mode 100644 configs/seeds/postgres/object/Hr_T_TrainingParticipant.sql create mode 100644 configs/seeds/postgres/object/Hr_T_TrainingSubject.sql create mode 100644 configs/seeds/postgres/object/Hr_T_TrainingType.sql create mode 100644 configs/seeds/postgres/object/Hr_T_Vehicle.sql create mode 100644 configs/seeds/postgres/object/Hr_T_VehicleDestination.sql create mode 100644 configs/seeds/postgres/object/Hr_T_VehicleDocument.sql create mode 100644 configs/seeds/postgres/object/Hr_T_VehicleExpense.sql create mode 100644 configs/seeds/postgres/object/Hr_T_VehicleExpenseDefinition.sql create mode 100644 configs/seeds/postgres/object/Hr_T_VehicleExpenseLine.sql create mode 100644 configs/seeds/postgres/object/Hr_T_VehicleFile.sql create mode 100644 configs/seeds/postgres/object/Hr_T_VehicleMovement.sql create mode 100644 configs/seeds/postgres/object/Hr_T_Visitor.sql create mode 100644 configs/seeds/postgres/object/Hr_T_WorkCalendar.sql create mode 100644 configs/seeds/postgres/object/Hr_V_AttendanceShift.sql create mode 100644 configs/seeds/postgres/object/Hr_V_LeaveBalanceYear.sql create mode 100644 configs/seeds/postgres/object/Hr_V_LeaveSummary.sql create mode 100644 configs/seeds/postgres/object/Hr_V_TimesheetMonthly.sql create mode 100644 configs/seeds/postgres/object/Prc_T_DeliveryMethod.sql create mode 100644 configs/seeds/postgres/object/Prc_T_Incoterm.sql create mode 100644 configs/seeds/postgres/object/Prc_T_PaymentMethod.sql create mode 100644 configs/seeds/postgres/object/Prc_T_PaymentTerm.sql delete mode 100644 configs/seeds/postgres/object/Prj_T_Customer.sql delete mode 100644 configs/seeds/postgres/object/Prj_T_Milestone.sql delete mode 100644 configs/seeds/postgres/object/Prj_T_Tag.sql delete mode 100644 configs/seeds/postgres/object/Prj_T_Task.sql delete mode 100644 configs/seeds/postgres/object/Prj_T_TaskDependency.sql delete mode 100644 configs/seeds/postgres/object/Prj_T_Timesheet.sql create mode 100644 ui/src/views/shared/lookupColor.tsx diff --git a/.github/instructions/ai.instructions.md b/.github/instructions/ai.instructions.md index 957e1203..680129a5 100644 --- a/.github/instructions/ai.instructions.md +++ b/.github/instructions/ai.instructions.md @@ -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. diff --git a/.github/instructions/dotnet.instructions.md b/.github/instructions/dotnet.instructions.md index 7bc85c8c..219f3172 100644 --- a/.github/instructions/dotnet.instructions.md +++ b/.github/instructions/dotnet.instructions.md @@ -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` / diff --git a/.github/instructions/lowcode.instructions.md b/.github/instructions/lowcode.instructions.md index 7bf5a80b..aca87d38 100644 --- a/.github/instructions/lowcode.instructions.md +++ b/.github/instructions/lowcode.instructions.md @@ -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 ` / `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 diff --git a/README.md b/README.md index bcf1def0..24d9c719 100644 --- a/README.md +++ b/README.md @@ -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). | diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpErrorCodes.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpErrorCodes.cs index 9689952a..c7bc315e 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpErrorCodes.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpErrorCodes.cs @@ -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"; } diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpTableCreator.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpTableCreator.cs index 68f2ad40..0a536dbc 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpTableCreator.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpTableCreator.cs @@ -17,7 +17,7 @@ namespace Sozsoft.Mcp.Services; /// /// Yeni tabloyu SQL Query Manager'daki tablo tasarimcisiyla ayni sozlesmeyle kurar: script /// tasarimcinin generateCreateTableSql ciktisiyla ayni bicimdedir ve tasarimcinin deploy -/// sonrasi yazdigi yere ({kapsam}/sql/object/{Tablo}.sql) yazilir; boylece tablo +/// sonrasi yazdigi yerlere (sql/object/{Tablo}.sql ve PostgreSQL esi postgres/object/{Tablo}.sql) yazilir; boylece tablo /// tasarimcida acilip duzenlenebilir ve veritabani sifirlandiginda seed ile geri gelir. /// /// 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( - "SELECT OBJECT_ID(@name, 'U')", + var existing = await context.Repository.ExecuteScalarAsync( + isPostgreSql ? "SELECT to_regclass(@name)::text" : "SELECT OBJECT_ID(@name, 'U')", context.ConnectionString, - new Dictionary { ["name"] = $"{SchemaName}.{tableName}" }); + new Dictionary { ["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}"; } - /// - /// Tasarimcinin SaveTableScriptAsync ile yazdigi yol. - /// Dosyasiz tablo veritabani sifirlandiginda geri gelmezdi; yazilamazsa islem hata verir. - /// - 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)) diff --git a/api/modules/Sozsoft.SqlQueryManager/Sozsoft.SqlQueryManager.Application/SqlObjectManagerAppService.cs b/api/modules/Sozsoft.SqlQueryManager/Sozsoft.SqlQueryManager.Application/SqlObjectManagerAppService.cs index ec2cea8c..a9e42cb6 100644 --- a/api/modules/Sozsoft.SqlQueryManager/Sozsoft.SqlQueryManager.Application/SqlObjectManagerAppService.cs +++ b/api/modules/Sozsoft.SqlQueryManager/Sozsoft.SqlQueryManager.Application/SqlObjectManagerAppService.cs @@ -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); } + /// + /// İki sağlayıcının object 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ı. + /// + private string[] ResolveSeedObjectPaths() => + [ + SeedPathResolver.GetPath(_configuration, SeedPathResolver.SqlFolder, SeedPathResolver.ObjectFolder), + SeedPathResolver.GetPath(_configuration, SeedPathResolver.PostgresFolder, SeedPathResolver.ObjectFolder), + ]; + } diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/WizardDataSeeder.cs b/api/src/Sozsoft.Platform.DbMigrator/Migrations/WizardDataSeeder.cs index cc57903f..939e527d 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/WizardDataSeeder.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/WizardDataSeeder.cs @@ -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(); 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); } /// diff --git a/api/src/Sozsoft.Platform.DbMigrator/PlatformDbMigratorModule.cs b/api/src/Sozsoft.Platform.DbMigrator/PlatformDbMigratorModule.cs index 6f516136..6591eeb6 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/PlatformDbMigratorModule.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/PlatformDbMigratorModule.cs @@ -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(); - - ConfigureVirtualFileSystem(context, hostEnvironment); + Configure(options => + { + options.FileSets.AddEmbedded(); + }); Configure(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(options => - { - options.FileSets.AddEmbedded(); - // if (hostEnvironment.IsDevelopment()) - // { - // options.FileSets.ReplaceEmbeddedByPhysical( - // Path.Combine(hostEnvironment.ContentRootPath, - // // $"..{Path.DirectorySeparatorChar}Sozsoft.Platform.DbMigrator")); - // $"..{Path.DirectorySeparatorChar}..{Path.DirectorySeparatorChar}..{Path.DirectorySeparatorChar}")); - // } - }); - } } diff --git a/api/src/Sozsoft.Platform.Domain/Data/ListFormSeedDataApplier.cs b/api/src/Sozsoft.Platform.Domain/Data/ListFormSeedDataApplier.cs index 7126f5bd..554600da 100644 --- a/api/src/Sozsoft.Platform.Domain/Data/ListFormSeedDataApplier.cs +++ b/api/src/Sozsoft.Platform.Domain/Data/ListFormSeedDataApplier.cs @@ -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( /// @p{sira}_{ad} olarak yeniden adlandirilir; boylece ekranin kendi insert komutu /// (InsertCommand) da degismeden toplu calisir. Komut basina parametre sayisi SQL Server /// sinirinin (2100) altinda tutulur. + /// + /// 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. + /// /// 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(@"(? parameters = []; private int rowCount; - public int ParameterCount => parameters.Count; + public bool IsFull => rowCount >= MaxRowCount || parameters.Count >= MaxParameterCount; public void Add(string rowSql, Dictionary rowParameters) { diff --git a/api/src/Sozsoft.Platform.Domain/Data/PostgreSqlTableScript.cs b/api/src/Sozsoft.Platform.Domain/Data/PostgreSqlTableScript.cs new file mode 100644 index 00000000..f293d671 --- /dev/null +++ b/api/src/Sozsoft.Platform.Domain/Data/PostgreSqlTableScript.cs @@ -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; + +/// +/// Tablo tasarımcısının ve MCP create_table'ın ürettiği SQL Server tablo scriptini +/// (sql/object/{Tablo}.sql) aynı tablonun PostgreSQL eşine (postgres/object/{Tablo}.sql) +/// ç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 +/// public, constraint pg_constraint kontrollü). +/// +/// Yalnızca tablo scripti biçimi tanınır: CREATE TABLE, ADD CONSTRAINT (CHECK / UNIQUE / +/// FOREIGN KEY) ve CREATE INDEX batch'leri. Görünüm, prosedür ve fonksiyon gövdesi diyalektler +/// arasında güvenle çevrilemez; tanınmayan batch'te false döner. +/// +/// +public static partial class PostgreSqlTableScript +{ + /// + /// Tablo scriptini iki sağlayıcının object 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 null döner; çağıran bunu bildirir. + /// + 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(); + 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; + } + + /// + /// Batch'i deyimlere ayırır: IF … BEGIN … END koruması kaldırılır, kalan metin + /// ALTER TABLE / CREATE TABLE / CREATE INDEX başlangıçlarından bölünür. + /// + private static IEnumerable 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 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(); + 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 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; + })); + + /// [x] → "x", N'..' → '..', bit kolon = 0/1 → = FALSE/TRUE. + private static string Expression(string expression, HashSet 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); + } + + /// [dbo].[X] → public."X", kalan [x] → "x". + 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>))*\)|(?:OBJECT_ID|INDEXPROPERTY)\s*\((?>[^()]+|\((?)|\)(?<-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+(?\S+)\s*\((?.*)\)\s*(?:ON\s+\[PRIMARY\])?\s*$", RegexOptions.Singleline | RegexOptions.IgnoreCase)] + private static partial Regex CreateTableRegex(); + + [GeneratedRegex(@"CONSTRAINT\s+\[(?[^\]]+)\]\s+PRIMARY\s+KEY(?:\s+(?:NON)?CLUSTERED)?\s*\((?[^)]*)\)", RegexOptions.Singleline | RegexOptions.IgnoreCase)] + private static partial Regex PrimaryKeyRegex(); + + [GeneratedRegex(@"^\s*\[(?[^\]]+)\]\s+(?[A-Za-z0-9]+(?:\s*\([^)]*\))?)(?\s+IDENTITY\s*\(\s*\d+\s*,\s*\d+\s*\))?(?\s+NOT\s+NULL|\s+NULL)?(?:\s+DEFAULT\s+(?.+?))?\s*,?\s*$", RegexOptions.IgnoreCase)] + private static partial Regex ColumnRegex(); + + [GeneratedRegex(@"^ALTER\s+TABLE\s+(?
\S+)\s+ADD\s+CONSTRAINT\s+\[(?[^\]]+)\]\s+(?(?:CHECK|UNIQUE|FOREIGN\s+KEY)\b.*)$", RegexOptions.Singleline | RegexOptions.IgnoreCase)] + private static partial Regex AddConstraintRegex(); + + [GeneratedRegex(@"^CREATE\s+(?UNIQUE\s+)?(?:(?:NON)?CLUSTERED\s+)?INDEX\s+\[(?[^\]]+)\]\s+ON\s+(?
\S+)\s*\((?[^)]*)\)(?:\s+INCLUDE\s*\((?[^)]*)\))?(?:\s+WHERE\s+(?.+?))?\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\]\.\[(?[^\]]+)\]", RegexOptions.IgnoreCase)] + private static partial Regex DboIdentifierRegex(); + + [GeneratedRegex(@"\[(?[^\]]+)\]")] + private static partial Regex BracketIdentifierRegex(); + + [GeneratedRegex(@"\bN'")] + private static partial Regex UnicodeLiteralRegex(); + + [GeneratedRegex(@"""(?[A-Za-z0-9_]+)""\s*=\s*(?[01])\b")] + private static partial Regex BitComparisonRegex(); + + [GeneratedRegex(@"\[(?[^\]]+)\]\s+bit\b", RegexOptions.IgnoreCase)] + private static partial Regex BitColumnRegex(); + + [GeneratedRegex(@"^(?[a-z]+)\((?[0-9,]+)\)$")] + private static partial Regex SizedTypeRegex(); + + [GeneratedRegex(@"\s+")] + private static partial Regex WhitespaceRegex(); +} diff --git a/api/src/Sozsoft.Platform.Domain/Queries/DefaultValueManager.cs b/api/src/Sozsoft.Platform.Domain/Queries/DefaultValueManager.cs index d00a39ac..d20953af 100644 --- a/api/src/Sozsoft.Platform.Domain/Queries/DefaultValueManager.cs +++ b/api/src/Sozsoft.Platform.Domain/Queries/DefaultValueManager.cs @@ -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) diff --git a/api/src/Sozsoft.Platform.Domain/Queries/QueryManager.cs b/api/src/Sozsoft.Platform.Domain/Queries/QueryManager.cs index c211d77b..18f50703 100644 --- a/api/src/Sozsoft.Platform.Domain/Queries/QueryManager.cs +++ b/api/src/Sozsoft.Platform.Domain/Queries/QueryManager.cs @@ -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 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, - // $@"(?(""[^""]+""|\[[^\]]+\]|`[^`]+`|\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, - // $@"(?(""[^""]+""|\[[^\]]+\]|`[^`]+`|\w+))\s*=\s*@{escapedParameterName}\b", - // match => $"{match.Groups["column"].Value} = ANY(@{parameterName})", - // RegexOptions.IgnoreCase); - // } } diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/DynamicData/SpRepository.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/DynamicData/SpRepository.cs index f6a22dfb..f4d19d65 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/DynamicData/SpRepository.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/DynamicData/SpRepository.cs @@ -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 parameters) + private static async Task 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 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 parameters) + private static string BuildSqlForPostgresql(string sp, Dictionary 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}"; + } + + /// + /// 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`. + /// + 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 parameters) diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs index a87984aa..3dbccc9f 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs @@ -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); } + /// + /// 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. + /// 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)); } } } diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PostgreSqlModelSyntax.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PostgreSqlModelSyntax.cs new file mode 100644 index 00000000..9a5c9247 --- /dev/null +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PostgreSqlModelSyntax.cs @@ -0,0 +1,70 @@ +using System.Text.RegularExpressions; + +namespace Sozsoft.Platform.EntityFrameworkCore; + +/// +/// 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 bu +/// çeviriyle kurulur. +/// +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, + }; + } + + /// + /// 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 + /// ([IsDeleted] = 0 → "IsDeleted" = FALSE). + /// + 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\((?\d+)\)$")] + private static partial Regex SizedStringRegex(); + + [GeneratedRegex(@"\[([^\]]+)\]")] + private static partial Regex BracketIdentifierRegex(); + + [GeneratedRegex(@"(?""Is[A-Za-z]+"")\s*=\s*(?[01])\b")] + private static partial Regex BitComparisonRegex(); +} diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/DbStartup/SetupMigrationRunner.cs b/api/src/Sozsoft.Platform.HttpApi.Host/DbStartup/SetupMigrationRunner.cs index 23ec339b..06c32623 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/DbStartup/SetupMigrationRunner.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/DbStartup/SetupMigrationRunner.cs @@ -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 /// Alt surece seed klasorunu bildiren ortam degiskeni (App:SeedsPath karsiligi). private const string SeedsPathEnvironmentVariable = "App__SeedsPath"; + private const string BuildStampFileName = "setup-migrator.stamp"; + + /// + /// Kaynak projeden çalışırken derleme çıktısını eskiten dosyalar. Seed klasörü çalışma anında + /// okunduğu için burada değildir. + /// + private static readonly HashSet SourceExtensions = new(StringComparer.OrdinalIgnoreCase) + { + ".cs", ".csproj", ".props", ".targets", ".json", ".resx" + }; + + /// Derleme çıktısı ya da çalışma anında yazılan klasörler; tazelik kararına girmez. + private static readonly HashSet IgnoredSourceDirectories = new(StringComparer.OrdinalIgnoreCase) + { + "bin", "obj", "Logs", "artifacts", "wwwroot", "node_modules", ".vs", ".git" + }; + /// /// Sürecin yaşam süresini kontrol eden token. Bu istemci isteğinin token'ı DEĞİL, /// uygulamanın ApplicationStopping 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("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 + { + // 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; + } + } + + /// + /// Kaynak projeden çalışırken migrator DLL'ini hazırlar. dotnet run 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. + /// + private static async Task 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; + } + + /// + /// 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). + /// + 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(); + } + + /// + /// 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. + /// + 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 EnumerateSourceFiles(DirectoryInfo root) + { + var pending = new Stack(); + 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); + } + } + } + } + + /// Süreci çalıştırır, stdout/stderr satırlarını kurulum loguna aktarır ve çıkış kodunu döndürür. + private static async Task RunProcessAsync( + MigratorCommand command, + IReadOnlyDictionary? 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); +/// Dolu ise komut kaynak projeden çalışır; Arguments yalnızca migrator argümanlarıdır. +internal sealed record MigratorCommand(string FileName, string Arguments, string WorkingDirectory, string? ProjectPath = null); diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs index 24139326..46b08b10 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs @@ -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; } + /// + /// Rengi olan lookup için renkli rozetler, renk kutusu için renk örneği; diğer alanlarda null. + /// + private static string GetColorMarkup(FormReportField field, IDictionary 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 row, diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportColorHelper.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportColorHelper.cs new file mode 100644 index 00000000..790dd467 --- /dev/null +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportColorHelper.cs @@ -0,0 +1,173 @@ +using System; +using System.Globalization; +using System.Linq; +using DevExpress.Data.Filtering; + +namespace Sozsoft.Reports.PredefinedReports; + +/// +/// Renk kutusu (dxColorBox) kolonlarını ve rengi olan lookup değerlerini DevExpress markup +/// metniyle çizer. Hücrenin AllowMarkupText özelliği açık olmalıdır. +/// +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. + + /// Lookup metni ile rengi arasındaki ayraç (SQL'de üretilir). + public const string PairSeparator = "\u241F"; + + /// Çok değerli lookup'ta öğeler arasındaki ayraç (SQL'de üretilir). + 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})"; + } + + /// Renk değerinin yanında küçük bir renk kutusu. + public static string ToSwatchMarkup(object value) + { + var text = ToText(value); + if (!TryParseColor(text, out var color)) + { + return text; + } + + return $"{Space}{Space}{Space}{Space}{Space}{Space}{text}"; + } + + /// "Metin␟Renk␞Metin␟Renk" çiftlerini renkli rozetlere çevirir. + 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 $"{Space}{label}{Space}"; + })); + } + + 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)); + } + } +} diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportLayoutHelper.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportLayoutHelper.cs index f90124f3..58377325 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportLayoutHelper.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportLayoutHelper.cs @@ -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; } + /// + /// Renkli çizilen kolonun markup ifadesi: rengi olan lookup rozet, renk kutusu renk örneği + /// olarak çizilir. Diğer kolonlarda null. + /// + 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 diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportSqlBuilder.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportSqlBuilder.cs index a64eafda..2038076a 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportSqlBuilder.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportSqlBuilder.cs @@ -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+(?:(?\[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"; } + /// + /// 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. + /// + internal static string GetLookupColorBindingName(ListFormField field) + { + return $"{field.FieldName}__LookupColor"; + } + + /// + /// Lookup sorgusu rengi AS [Color] 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. + /// + 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); + } + + /// + /// Görünen metin ile rengi 'ın çözdüğü ayraçla birleştirir. + /// + 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) diff --git a/claude.md b/claude.md index 3cba6971..9c0864d1 100644 --- a/claude.md +++ b/claude.md @@ -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 diff --git a/configs/seeds/data/App.Languages.LanguageText.json b/configs/seeds/data/App.Languages.LanguageText.json index 8301b6f4..7841c5e7 100644 --- a/configs/seeds/data/App.Languages.LanguageText.json +++ b/configs/seeds/data/App.Languages.LanguageText.json @@ -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", diff --git a/configs/seeds/postgres/execute/Sas_H_ListFormFieldsDictionaryJson_SaveDbTableColumns.sql b/configs/seeds/postgres/execute/Sas_H_ListFormFieldsDictionaryJson_SaveDbTableColumns.sql index 6a72d387..0ba6a2bf 100644 --- a/configs/seeds/postgres/execute/Sas_H_ListFormFieldsDictionaryJson_SaveDbTableColumns.sql +++ b/configs/seeds/postgres/execute/Sas_H_ListFormFieldsDictionaryJson_SaveDbTableColumns.sql @@ -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; -$$; \ No newline at end of file +$$; diff --git a/configs/seeds/postgres/host/object/Sas_H_BackgroundWorker_JobFlow.sql b/configs/seeds/postgres/host/object/Sas_H_BackgroundWorker_JobFlow.sql index 5cfaca5a..6f1c8917 100644 --- a/configs/seeds/postgres/host/object/Sas_H_BackgroundWorker_JobFlow.sql +++ b/configs/seeds/postgres/host/object/Sas_H_BackgroundWorker_JobFlow.sql @@ -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; $$; diff --git a/configs/seeds/postgres/object/Adm_T_Contact.sql b/configs/seeds/postgres/object/Adm_T_Contact.sql new file mode 100644 index 00000000..40611a7f --- /dev/null +++ b/configs/seeds/postgres/object/Adm_T_Contact.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Adm_T_ContactBankAccount.sql b/configs/seeds/postgres/object/Adm_T_ContactBankAccount.sql new file mode 100644 index 00000000..67734a3c --- /dev/null +++ b/configs/seeds/postgres/object/Adm_T_ContactBankAccount.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Adm_T_ContactPerson.sql b/configs/seeds/postgres/object/Adm_T_ContactPerson.sql new file mode 100644 index 00000000..28609245 --- /dev/null +++ b/configs/seeds/postgres/object/Adm_T_ContactPerson.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Adm_T_ContactTag.sql b/configs/seeds/postgres/object/Adm_T_ContactTag.sql new file mode 100644 index 00000000..a99f8689 --- /dev/null +++ b/configs/seeds/postgres/object/Adm_T_ContactTag.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Hr_F_HrSetting.sql b/configs/seeds/postgres/object/Hr_F_HrSetting.sql new file mode 100644 index 00000000..b9df4db6 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_F_HrSetting.sql @@ -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; +$$; diff --git a/configs/seeds/postgres/object/Hr_F_LeaveNetDays.sql b/configs/seeds/postgres/object/Hr_F_LeaveNetDays.sql new file mode 100644 index 00000000..1b6c8ceb --- /dev/null +++ b/configs/seeds/postgres/object/Hr_F_LeaveNetDays.sql @@ -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; +$$; diff --git a/configs/seeds/postgres/object/Hr_F_LeaveWorkDays.sql b/configs/seeds/postgres/object/Hr_F_LeaveWorkDays.sql new file mode 100644 index 00000000..5a1ea0d6 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_F_LeaveWorkDays.sql @@ -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; +$$; diff --git a/configs/seeds/postgres/object/Hr_F_TimesheetPeriodClosed.sql b/configs/seeds/postgres/object/Hr_F_TimesheetPeriodClosed.sql new file mode 100644 index 00000000..449cc1b8 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_F_TimesheetPeriodClosed.sql @@ -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; +$$; diff --git a/configs/seeds/postgres/object/Hr_F_WorkCalendarDayType.sql b/configs/seeds/postgres/object/Hr_F_WorkCalendarDayType.sql new file mode 100644 index 00000000..5a4e980f --- /dev/null +++ b/configs/seeds/postgres/object/Hr_F_WorkCalendarDayType.sql @@ -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; +$$; diff --git a/configs/seeds/postgres/object/Hr_S_AttendanceImport.sql b/configs/seeds/postgres/object/Hr_S_AttendanceImport.sql new file mode 100644 index 00000000..f9b109a2 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_S_AttendanceImport.sql @@ -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; +$$; diff --git a/configs/seeds/postgres/object/Hr_S_AttendanceProcess.sql b/configs/seeds/postgres/object/Hr_S_AttendanceProcess.sql new file mode 100644 index 00000000..9114c1ca --- /dev/null +++ b/configs/seeds/postgres/object/Hr_S_AttendanceProcess.sql @@ -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; +$$; diff --git a/configs/seeds/postgres/object/Hr_S_DevelopmentPlanSync.sql b/configs/seeds/postgres/object/Hr_S_DevelopmentPlanSync.sql new file mode 100644 index 00000000..68c22add --- /dev/null +++ b/configs/seeds/postgres/object/Hr_S_DevelopmentPlanSync.sql @@ -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 +$$; diff --git a/configs/seeds/postgres/object/Hr_S_LeaveDaysRefresh.sql b/configs/seeds/postgres/object/Hr_S_LeaveDaysRefresh.sql new file mode 100644 index 00000000..6ef9e4dc --- /dev/null +++ b/configs/seeds/postgres/object/Hr_S_LeaveDaysRefresh.sql @@ -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 +$$; diff --git a/configs/seeds/postgres/object/Hr_S_LeaveNotify.sql b/configs/seeds/postgres/object/Hr_S_LeaveNotify.sql new file mode 100644 index 00000000..7dc23b8d --- /dev/null +++ b/configs/seeds/postgres/object/Hr_S_LeaveNotify.sql @@ -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 +$$; diff --git a/configs/seeds/postgres/object/Hr_S_LeaveRequestAfterSave.sql b/configs/seeds/postgres/object/Hr_S_LeaveRequestAfterSave.sql new file mode 100644 index 00000000..4a590450 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_S_LeaveRequestAfterSave.sql @@ -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 +$$; diff --git a/configs/seeds/postgres/object/Hr_S_LeaveRequestGuard.sql b/configs/seeds/postgres/object/Hr_S_LeaveRequestGuard.sql new file mode 100644 index 00000000..ff956aca --- /dev/null +++ b/configs/seeds/postgres/object/Hr_S_LeaveRequestGuard.sql @@ -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 +$$; diff --git a/configs/seeds/postgres/object/Hr_S_LeaveSickLeaveSync.sql b/configs/seeds/postgres/object/Hr_S_LeaveSickLeaveSync.sql new file mode 100644 index 00000000..1221be6c --- /dev/null +++ b/configs/seeds/postgres/object/Hr_S_LeaveSickLeaveSync.sql @@ -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 +$$; diff --git a/configs/seeds/postgres/object/Hr_S_PerformanceFeedbackOpen.sql b/configs/seeds/postgres/object/Hr_S_PerformanceFeedbackOpen.sql new file mode 100644 index 00000000..558d89c9 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_S_PerformanceFeedbackOpen.sql @@ -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 +$$; diff --git a/configs/seeds/postgres/object/Hr_S_PerformanceGuard.sql b/configs/seeds/postgres/object/Hr_S_PerformanceGuard.sql new file mode 100644 index 00000000..8a4b2fc9 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_S_PerformanceGuard.sql @@ -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 +$$; diff --git a/configs/seeds/postgres/object/Hr_S_PerformanceNotify.sql b/configs/seeds/postgres/object/Hr_S_PerformanceNotify.sql new file mode 100644 index 00000000..4b2c38a5 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_S_PerformanceNotify.sql @@ -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 +$$; diff --git a/configs/seeds/postgres/object/Hr_S_PerformancePeriodActivate.sql b/configs/seeds/postgres/object/Hr_S_PerformancePeriodActivate.sql new file mode 100644 index 00000000..e4b8e7cc --- /dev/null +++ b/configs/seeds/postgres/object/Hr_S_PerformancePeriodActivate.sql @@ -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 +$$; diff --git a/configs/seeds/postgres/object/Hr_S_PerformanceReviewAfterSave.sql b/configs/seeds/postgres/object/Hr_S_PerformanceReviewAfterSave.sql new file mode 100644 index 00000000..b41da11b --- /dev/null +++ b/configs/seeds/postgres/object/Hr_S_PerformanceReviewAfterSave.sql @@ -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 +$$; diff --git a/configs/seeds/postgres/object/Hr_S_PerformanceReviewRecalc.sql b/configs/seeds/postgres/object/Hr_S_PerformanceReviewRecalc.sql new file mode 100644 index 00000000..7133e19a --- /dev/null +++ b/configs/seeds/postgres/object/Hr_S_PerformanceReviewRecalc.sql @@ -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 +$$; diff --git a/configs/seeds/postgres/object/Hr_S_TimesheetCalculate.sql b/configs/seeds/postgres/object/Hr_S_TimesheetCalculate.sql new file mode 100644 index 00000000..e2fd3aba --- /dev/null +++ b/configs/seeds/postgres/object/Hr_S_TimesheetCalculate.sql @@ -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; +$$; diff --git a/configs/seeds/postgres/object/Hr_S_TimesheetNightly.sql b/configs/seeds/postgres/object/Hr_S_TimesheetNightly.sql new file mode 100644 index 00000000..5bd2e304 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_S_TimesheetNightly.sql @@ -0,0 +1,70 @@ +-- Zamanlanmış işin BeforeSp'si PostgreSQL'de "SELECT * FROM public.()" 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; +$$; diff --git a/configs/seeds/postgres/object/Hr_S_TimesheetPeriodAfterSave.sql b/configs/seeds/postgres/object/Hr_S_TimesheetPeriodAfterSave.sql new file mode 100644 index 00000000..3a44971f --- /dev/null +++ b/configs/seeds/postgres/object/Hr_S_TimesheetPeriodAfterSave.sql @@ -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; +$$; diff --git a/configs/seeds/postgres/object/Hr_S_TrainingSync.sql b/configs/seeds/postgres/object/Hr_S_TrainingSync.sql new file mode 100644 index 00000000..4c12cd55 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_S_TrainingSync.sql @@ -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 +$$; diff --git a/configs/seeds/postgres/object/Hr_S_WorkCalendarAfterSave.sql b/configs/seeds/postgres/object/Hr_S_WorkCalendarAfterSave.sql new file mode 100644 index 00000000..5d389b49 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_S_WorkCalendarAfterSave.sql @@ -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; +$$; diff --git a/configs/seeds/postgres/object/Hr_T_AttendanceLog.sql b/configs/seeds/postgres/object/Hr_T_AttendanceLog.sql new file mode 100644 index 00000000..897601b2 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_AttendanceLog.sql @@ -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; diff --git a/configs/seeds/postgres/object/Hr_T_BenefitType.sql b/configs/seeds/postgres/object/Hr_T_BenefitType.sql new file mode 100644 index 00000000..d7982cb2 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_BenefitType.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Hr_T_Candidate.sql b/configs/seeds/postgres/object/Hr_T_Candidate.sql new file mode 100644 index 00000000..5aa4f208 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_Candidate.sql @@ -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; diff --git a/configs/seeds/postgres/object/Hr_T_CandidateSource.sql b/configs/seeds/postgres/object/Hr_T_CandidateSource.sql new file mode 100644 index 00000000..286f16cf --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_CandidateSource.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Hr_T_Certificate.sql b/configs/seeds/postgres/object/Hr_T_Certificate.sql new file mode 100644 index 00000000..34e0541c --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_Certificate.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Hr_T_DevelopmentPlan.sql b/configs/seeds/postgres/object/Hr_T_DevelopmentPlan.sql new file mode 100644 index 00000000..c60ce58e --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_DevelopmentPlan.sql @@ -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"); diff --git a/configs/seeds/postgres/object/Hr_T_DisciplinaryCase.sql b/configs/seeds/postgres/object/Hr_T_DisciplinaryCase.sql new file mode 100644 index 00000000..0bbb6b2d --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_DisciplinaryCase.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Hr_T_DisciplinaryDecision.sql b/configs/seeds/postgres/object/Hr_T_DisciplinaryDecision.sql new file mode 100644 index 00000000..c493ec1d --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_DisciplinaryDecision.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Hr_T_DisciplinaryDefense.sql b/configs/seeds/postgres/object/Hr_T_DisciplinaryDefense.sql new file mode 100644 index 00000000..e5cdd7de --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_DisciplinaryDefense.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Hr_T_DisciplinaryFile.sql b/configs/seeds/postgres/object/Hr_T_DisciplinaryFile.sql new file mode 100644 index 00000000..c4b07b0d --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_DisciplinaryFile.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Hr_T_DisciplinaryPenaltyType.sql b/configs/seeds/postgres/object/Hr_T_DisciplinaryPenaltyType.sql new file mode 100644 index 00000000..ed247355 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_DisciplinaryPenaltyType.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Hr_T_Doctor.sql b/configs/seeds/postgres/object/Hr_T_Doctor.sql new file mode 100644 index 00000000..fde0a36e --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_Doctor.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Hr_T_DocumentType.sql b/configs/seeds/postgres/object/Hr_T_DocumentType.sql new file mode 100644 index 00000000..17afe926 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_DocumentType.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Hr_T_Driver.sql b/configs/seeds/postgres/object/Hr_T_Driver.sql new file mode 100644 index 00000000..146ff9fb --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_Driver.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Hr_T_DriverAccident.sql b/configs/seeds/postgres/object/Hr_T_DriverAccident.sql new file mode 100644 index 00000000..0a141b60 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_DriverAccident.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Hr_T_DriverDocument.sql b/configs/seeds/postgres/object/Hr_T_DriverDocument.sql new file mode 100644 index 00000000..fc1275d7 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_DriverDocument.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Hr_T_DriverPenalty.sql b/configs/seeds/postgres/object/Hr_T_DriverPenalty.sql new file mode 100644 index 00000000..ef031aa8 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_DriverPenalty.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Hr_T_EvaluationCriterion.sql b/configs/seeds/postgres/object/Hr_T_EvaluationCriterion.sql new file mode 100644 index 00000000..117ebb0a --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_EvaluationCriterion.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Hr_T_EventLog.sql b/configs/seeds/postgres/object/Hr_T_EventLog.sql new file mode 100644 index 00000000..99f5f73d --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_EventLog.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Hr_T_ExpenseType.sql b/configs/seeds/postgres/object/Hr_T_ExpenseType.sql new file mode 100644 index 00000000..ed81ff86 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_ExpenseType.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Hr_T_Hospital.sql b/configs/seeds/postgres/object/Hr_T_Hospital.sql new file mode 100644 index 00000000..3fdeeb3b --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_Hospital.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Hr_T_HospitalDepartment.sql b/configs/seeds/postgres/object/Hr_T_HospitalDepartment.sql new file mode 100644 index 00000000..dbed6176 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_HospitalDepartment.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Hr_T_Interview.sql b/configs/seeds/postgres/object/Hr_T_Interview.sql new file mode 100644 index 00000000..e9d21dc5 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_Interview.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Hr_T_InterviewScore.sql b/configs/seeds/postgres/object/Hr_T_InterviewScore.sql new file mode 100644 index 00000000..442607a2 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_InterviewScore.sql @@ -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; diff --git a/configs/seeds/postgres/object/Hr_T_JobApplication.sql b/configs/seeds/postgres/object/Hr_T_JobApplication.sql new file mode 100644 index 00000000..568df714 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_JobApplication.sql @@ -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; diff --git a/configs/seeds/postgres/object/Hr_T_JobOffer.sql b/configs/seeds/postgres/object/Hr_T_JobOffer.sql new file mode 100644 index 00000000..edc15a0b --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_JobOffer.sql @@ -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 +$$; diff --git a/configs/seeds/postgres/object/Hr_T_JobPosting.sql b/configs/seeds/postgres/object/Hr_T_JobPosting.sql new file mode 100644 index 00000000..4e3ee295 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_JobPosting.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Hr_T_LeaveBalanceAdjustment.sql b/configs/seeds/postgres/object/Hr_T_LeaveBalanceAdjustment.sql new file mode 100644 index 00000000..aa950931 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_LeaveBalanceAdjustment.sql @@ -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; diff --git a/configs/seeds/postgres/object/Hr_T_LeaveRequest.sql b/configs/seeds/postgres/object/Hr_T_LeaveRequest.sql new file mode 100644 index 00000000..6a08b4a7 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_LeaveRequest.sql @@ -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; diff --git a/configs/seeds/postgres/object/Hr_T_LeaveType.sql b/configs/seeds/postgres/object/Hr_T_LeaveType.sql new file mode 100644 index 00000000..b40840ee --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_LeaveType.sql @@ -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 +$$; diff --git a/configs/seeds/postgres/object/Hr_T_PerformanceCompetency.sql b/configs/seeds/postgres/object/Hr_T_PerformanceCompetency.sql new file mode 100644 index 00000000..02700c31 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_PerformanceCompetency.sql @@ -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; diff --git a/configs/seeds/postgres/object/Hr_T_PerformanceFeedback.sql b/configs/seeds/postgres/object/Hr_T_PerformanceFeedback.sql new file mode 100644 index 00000000..14057b5c --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_PerformanceFeedback.sql @@ -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"); diff --git a/configs/seeds/postgres/object/Hr_T_PerformanceFeedbackScore.sql b/configs/seeds/postgres/object/Hr_T_PerformanceFeedbackScore.sql new file mode 100644 index 00000000..53668781 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_PerformanceFeedbackScore.sql @@ -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; diff --git a/configs/seeds/postgres/object/Hr_T_PerformanceGoal.sql b/configs/seeds/postgres/object/Hr_T_PerformanceGoal.sql new file mode 100644 index 00000000..79c2482f --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_PerformanceGoal.sql @@ -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"); diff --git a/configs/seeds/postgres/object/Hr_T_PerformancePeriod.sql b/configs/seeds/postgres/object/Hr_T_PerformancePeriod.sql new file mode 100644 index 00000000..b6018980 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_PerformancePeriod.sql @@ -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 +$$; diff --git a/configs/seeds/postgres/object/Hr_T_PerformancePeriodSkill.sql b/configs/seeds/postgres/object/Hr_T_PerformancePeriodSkill.sql new file mode 100644 index 00000000..dc37a9e9 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_PerformancePeriodSkill.sql @@ -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; diff --git a/configs/seeds/postgres/object/Hr_T_PerformanceRating.sql b/configs/seeds/postgres/object/Hr_T_PerformanceRating.sql new file mode 100644 index 00000000..3f2153b1 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_PerformanceRating.sql @@ -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; diff --git a/configs/seeds/postgres/object/Hr_T_PerformanceReview.sql b/configs/seeds/postgres/object/Hr_T_PerformanceReview.sql new file mode 100644 index 00000000..a93beed5 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_PerformanceReview.sql @@ -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"); diff --git a/configs/seeds/postgres/object/Hr_T_RejectionReason.sql b/configs/seeds/postgres/object/Hr_T_RejectionReason.sql new file mode 100644 index 00000000..dd14335c --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_RejectionReason.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Hr_T_Shift.sql b/configs/seeds/postgres/object/Hr_T_Shift.sql new file mode 100644 index 00000000..421679c7 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_Shift.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Hr_T_StaffRequest.sql b/configs/seeds/postgres/object/Hr_T_StaffRequest.sql new file mode 100644 index 00000000..449cfaa8 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_StaffRequest.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Hr_T_Timesheet.sql b/configs/seeds/postgres/object/Hr_T_Timesheet.sql new file mode 100644 index 00000000..937b598c --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_Timesheet.sql @@ -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"); diff --git a/configs/seeds/postgres/object/Hr_T_TimesheetPeriod.sql b/configs/seeds/postgres/object/Hr_T_TimesheetPeriod.sql new file mode 100644 index 00000000..63876644 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_TimesheetPeriod.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Hr_T_Training.sql b/configs/seeds/postgres/object/Hr_T_Training.sql new file mode 100644 index 00000000..db231685 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_Training.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Hr_T_TrainingFile.sql b/configs/seeds/postgres/object/Hr_T_TrainingFile.sql new file mode 100644 index 00000000..0235d16b --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_TrainingFile.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Hr_T_TrainingParticipant.sql b/configs/seeds/postgres/object/Hr_T_TrainingParticipant.sql new file mode 100644 index 00000000..b3acc58c --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_TrainingParticipant.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Hr_T_TrainingSubject.sql b/configs/seeds/postgres/object/Hr_T_TrainingSubject.sql new file mode 100644 index 00000000..ba25b2c7 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_TrainingSubject.sql @@ -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") +); diff --git a/configs/seeds/postgres/object/Hr_T_TrainingType.sql b/configs/seeds/postgres/object/Hr_T_TrainingType.sql new file mode 100644 index 00000000..54fd95b7 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_TrainingType.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS public."Hr_T_TrainingType" +( + "Id" uuid NOT NULL DEFAULT gen_random_uuid(), + "TenantId" uuid NULL, + "Name" varchar(128) 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_TrainingType" PRIMARY KEY ("Id") +); diff --git a/configs/seeds/postgres/object/Hr_T_Vehicle.sql b/configs/seeds/postgres/object/Hr_T_Vehicle.sql new file mode 100644 index 00000000..1726e7ca --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_Vehicle.sql @@ -0,0 +1,32 @@ +CREATE TABLE IF NOT EXISTS public."Hr_T_Vehicle" +( + "Id" uuid NOT NULL DEFAULT gen_random_uuid(), + "TenantId" uuid NULL, + "BranchId" uuid NOT NULL, + "Name" varchar(128) NOT NULL, + "Brand" varchar(64) NULL, + "Model" varchar(64) NULL, + "BodyType" varchar(64) NULL, + "UsagePlace" varchar(64) NULL, + "RegistrationDate" date NULL, + "PlateNumber" varchar(16) NOT NULL, + "IsActive" boolean NOT NULL DEFAULT TRUE, + "Status" varchar(16) NOT NULL DEFAULT 'Arrived', + "CurrentKm" integer NOT NULL DEFAULT 0, + "InspectionStart" date NULL, + "InspectionEnd" date NULL, + "InsuranceStart" date NULL, + "InsuranceEnd" date NULL, + "EmissionStart" date NULL, + "EmissionEnd" date NULL, + "TachographStart" date NULL, + "TachographEnd" date 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_Vehicle" PRIMARY KEY ("Id") +); diff --git a/configs/seeds/postgres/object/Hr_T_VehicleDestination.sql b/configs/seeds/postgres/object/Hr_T_VehicleDestination.sql new file mode 100644 index 00000000..69cab7f1 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_VehicleDestination.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS public."Hr_T_VehicleDestination" +( + "Id" uuid NOT NULL DEFAULT gen_random_uuid(), + "TenantId" uuid NULL, + "Name" varchar(128) NOT NULL, + "Distance" numeric(18, 4) 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_VehicleDestination" PRIMARY KEY ("Id") +); diff --git a/configs/seeds/postgres/object/Hr_T_VehicleDocument.sql b/configs/seeds/postgres/object/Hr_T_VehicleDocument.sql new file mode 100644 index 00000000..f3affcec --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_VehicleDocument.sql @@ -0,0 +1,17 @@ +CREATE TABLE IF NOT EXISTS public."Hr_T_VehicleDocument" +( + "Id" uuid NOT NULL DEFAULT gen_random_uuid(), + "TenantId" uuid NULL, + "DocumentGroup" varchar(16) NOT NULL, + "DocumentType" varchar(32) NOT NULL, + "Name" varchar(128) 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_VehicleDocument" PRIMARY KEY ("Id") +); diff --git a/configs/seeds/postgres/object/Hr_T_VehicleExpense.sql b/configs/seeds/postgres/object/Hr_T_VehicleExpense.sql new file mode 100644 index 00000000..5b74b4eb --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_VehicleExpense.sql @@ -0,0 +1,20 @@ +CREATE TABLE IF NOT EXISTS public."Hr_T_VehicleExpense" +( + "Id" uuid NOT NULL DEFAULT gen_random_uuid(), + "TenantId" uuid NULL, + "VehicleId" uuid NOT NULL, + "InvoiceNo" varchar(32) NOT NULL, + "InvoiceDate" date NOT NULL, + "DestinationId" uuid 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_VehicleExpense" PRIMARY KEY ("Id") +); diff --git a/configs/seeds/postgres/object/Hr_T_VehicleExpenseDefinition.sql b/configs/seeds/postgres/object/Hr_T_VehicleExpenseDefinition.sql new file mode 100644 index 00000000..63721c74 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_VehicleExpenseDefinition.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS public."Hr_T_VehicleExpenseDefinition" +( + "Id" uuid NOT NULL DEFAULT gen_random_uuid(), + "TenantId" uuid NULL, + "ExpenseType" varchar(32) NOT NULL, + "Name" varchar(128) 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_VehicleExpenseDefinition" PRIMARY KEY ("Id") +); diff --git a/configs/seeds/postgres/object/Hr_T_VehicleExpenseLine.sql b/configs/seeds/postgres/object/Hr_T_VehicleExpenseLine.sql new file mode 100644 index 00000000..73dc1ffa --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_VehicleExpenseLine.sql @@ -0,0 +1,21 @@ +CREATE TABLE IF NOT EXISTS public."Hr_T_VehicleExpenseLine" +( + "Id" uuid NOT NULL DEFAULT gen_random_uuid(), + "TenantId" uuid NULL, + "VehicleExpenseId" uuid NOT NULL, + "ExpenseDefinitionId" uuid NOT NULL, + "DetailName" varchar(128) NULL, + "Quantity" numeric(18, 4) NOT NULL DEFAULT 1, + "Amount" numeric(18, 4) NOT NULL DEFAULT 0, + "DiscountRate" numeric(18, 4) NOT NULL DEFAULT 0, + "NetAmount" numeric(18, 4) NOT NULL DEFAULT 0, + "Description" 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_VehicleExpenseLine" PRIMARY KEY ("Id") +); diff --git a/configs/seeds/postgres/object/Hr_T_VehicleFile.sql b/configs/seeds/postgres/object/Hr_T_VehicleFile.sql new file mode 100644 index 00000000..feb44b06 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_VehicleFile.sql @@ -0,0 +1,17 @@ +CREATE TABLE IF NOT EXISTS public."Hr_T_VehicleFile" +( + "Id" uuid NOT NULL DEFAULT gen_random_uuid(), + "TenantId" uuid NULL, + "VehicleId" 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_VehicleFile" PRIMARY KEY ("Id") +); diff --git a/configs/seeds/postgres/object/Hr_T_VehicleMovement.sql b/configs/seeds/postgres/object/Hr_T_VehicleMovement.sql new file mode 100644 index 00000000..87e29bfb --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_VehicleMovement.sql @@ -0,0 +1,34 @@ +CREATE TABLE IF NOT EXISTS public."Hr_T_VehicleMovement" +( + "Id" uuid NOT NULL DEFAULT gen_random_uuid(), + "TenantId" uuid NULL, + "MovementNo" integer NOT NULL DEFAULT 0, + "VehicleId" uuid NOT NULL, + "BranchId" uuid NULL, + "ExitDate" date NOT NULL, + "ExitTime" varchar(5) NOT NULL, + "ExitKm" integer NOT NULL, + "SenderUserId" uuid NULL, + "SecurityUserId" uuid NULL, + "DriverUserId" uuid NOT NULL, + "DestinationId" uuid NULL, + "Reason" text NULL, + "Location" varchar(128) NULL, + "EntrySecurityUserId" uuid NULL, + "EntryBranchId" uuid NULL, + "EntryDate" date NULL, + "EntryTime" varchar(5) NULL, + "EntryKm" integer NULL, + "TravelledKm" integer NULL, + "Status" varchar(16) NOT NULL DEFAULT 'Left', + "DurationMinutes" integer NULL, + "ExpectedKm" integer 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_VehicleMovement" PRIMARY KEY ("Id") +); diff --git a/configs/seeds/postgres/object/Hr_T_Visitor.sql b/configs/seeds/postgres/object/Hr_T_Visitor.sql new file mode 100644 index 00000000..55ad64e7 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_Visitor.sql @@ -0,0 +1,23 @@ +CREATE TABLE IF NOT EXISTS public."Hr_T_Visitor" +( + "Id" uuid NOT NULL DEFAULT gen_random_uuid(), + "TenantId" uuid NULL, + "BranchId" uuid NOT NULL, + "VisitorType" varchar(50) NOT NULL DEFAULT 'Ziyaretçi', + "VisitDate" date NOT NULL, + "EntryTime" timestamp NOT NULL, + "ExitTime" timestamp NULL, + "CardNo" varchar(20) NULL, + "VisitorFullName" varchar(200) NOT NULL, + "VisitedUserId" uuid NOT NULL, + "CompanyName" varchar(200) NULL, + "VisitReason" 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_Visitor" PRIMARY KEY ("Id") +); diff --git a/configs/seeds/postgres/object/Hr_T_WorkCalendar.sql b/configs/seeds/postgres/object/Hr_T_WorkCalendar.sql new file mode 100644 index 00000000..9ab454fa --- /dev/null +++ b/configs/seeds/postgres/object/Hr_T_WorkCalendar.sql @@ -0,0 +1,20 @@ +CREATE TABLE IF NOT EXISTS public."Hr_T_WorkCalendar" +( + "Id" uuid NOT NULL DEFAULT gen_random_uuid(), + "TenantId" uuid NULL, + "Name" varchar(128) NOT NULL, + "StartDate" date NOT NULL, + "EndDate" date NOT NULL, + "AllDay" boolean NOT NULL DEFAULT TRUE, + "DayType" varchar(16) NOT NULL DEFAULT 'Holiday', + "RecurrenceRule" varchar(256) 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_WorkCalendar" PRIMARY KEY ("Id") +); diff --git a/configs/seeds/postgres/object/Hr_V_AttendanceShift.sql b/configs/seeds/postgres/object/Hr_V_AttendanceShift.sql new file mode 100644 index 00000000..9442f87e --- /dev/null +++ b/configs/seeds/postgres/object/Hr_V_AttendanceShift.sql @@ -0,0 +1,39 @@ +CREATE OR REPLACE VIEW public."Hr_V_AttendanceShift" +AS +-- Çalışanın puantajda kullanılan vardiyası (dakika cinsinden). Kullanıcı kartında vardiya yoksa ya da +-- vardiyanın alanı boşsa Ayarlar › İnsan Kaynakları › Puantaj'daki varsayılan geçerlidir. +-- DayOffset: hareketin hangi iş gününe yazılacağı. Vardiya bitişi ile bir sonraki başlangıç arasındaki +-- boşluğun ortası gün sınırıdır; gece vardiyasındaki 23:50 okutması vardiyanın başladığı ertesi günün +-- hareketidir. İş günü = ("Zaman" - "DayOffset" * INTERVAL '1 minute')::date. +-- Geçersiz metin (TRY_CONVERT karşılığı) pg_input_is_valid ile NULL'a düşer. +SELECT x."EmployeeId", x."TenantId", x."ShiftId", x."StartMin", x."EndMin", x."BreakMin", x."LateTol", x."EarlyTol", + x."StartMin" - ((x."StartMin" - ((x."EndMin" + ((x."StartMin" - x."EndMin" + 1440) % 1440) / 2) % 1440) + 1440) % 1440) AS "DayOffset" +FROM "AbpUsers" u +LEFT JOIN "Hr_T_Shift" s ON s."Id" = u."ShiftId" AND s."IsDeleted" = FALSE +CROSS JOIN LATERAL ( + SELECT public."Hr_F_HrSetting"(u."TenantId", 'App.HrSettings.Attendance.DefaultShiftStart') AS DefaultStartText, + public."Hr_F_HrSetting"(u."TenantId", 'App.HrSettings.Attendance.DefaultShiftEnd') AS DefaultEndText +) h +CROSS JOIN LATERAL ( + SELECT CASE WHEN pg_input_is_valid(s."StartTime", 'time') THEN s."StartTime"::time END AS ShiftStart, + CASE WHEN pg_input_is_valid(s."EndTime", 'time') THEN s."EndTime"::time END AS ShiftEnd, + CASE WHEN pg_input_is_valid(h.DefaultStartText, 'time') THEN h.DefaultStartText::time END AS DefaultStart, + CASE WHEN pg_input_is_valid(h.DefaultEndText, 'time') THEN h.DefaultEndText::time END AS DefaultEnd +) t +CROSS JOIN LATERAL ( + SELECT u."Id" AS "EmployeeId", u."TenantId", s."Id" AS "ShiftId", + COALESCE(EXTRACT(HOUR FROM t.ShiftStart)::int * 60 + EXTRACT(MINUTE FROM t.ShiftStart)::int, + EXTRACT(HOUR FROM t.DefaultStart)::int * 60 + EXTRACT(MINUTE FROM t.DefaultStart)::int, 480) AS "StartMin", + COALESCE(EXTRACT(HOUR FROM t.ShiftEnd)::int * 60 + EXTRACT(MINUTE FROM t.ShiftEnd)::int, + EXTRACT(HOUR FROM t.DefaultEnd)::int * 60 + EXTRACT(MINUTE FROM t.DefaultEnd)::int, 1080) AS "EndMin", + COALESCE(s."BreakMinutes", + (SELECT CASE WHEN pg_input_is_valid(z.v, 'integer') THEN z.v::int END + FROM (SELECT public."Hr_F_HrSetting"(u."TenantId", 'App.HrSettings.Attendance.DefaultBreakMinutes') AS v) z), 60) AS "BreakMin", + COALESCE(s."LateToleranceMinutes", + (SELECT CASE WHEN pg_input_is_valid(z.v, 'integer') THEN z.v::int END + FROM (SELECT public."Hr_F_HrSetting"(u."TenantId", 'App.HrSettings.Attendance.LateToleranceMinutes') AS v) z), 10) AS "LateTol", + COALESCE(s."EarlyLeaveToleranceMinutes", + (SELECT CASE WHEN pg_input_is_valid(z.v, 'integer') THEN z.v::int END + FROM (SELECT public."Hr_F_HrSetting"(u."TenantId", 'App.HrSettings.Attendance.EarlyLeaveToleranceMinutes') AS v) z), 10) AS "EarlyTol" +) x +WHERE u."IsDeleted" = FALSE; diff --git a/configs/seeds/postgres/object/Hr_V_LeaveBalanceYear.sql b/configs/seeds/postgres/object/Hr_V_LeaveBalanceYear.sql new file mode 100644 index 00000000..555aaa53 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_V_LeaveBalanceYear.sql @@ -0,0 +1,124 @@ +CREATE OR REPLACE VIEW public."Hr_V_LeaveBalanceYear" +AS +-- Yıllık izin defteri: çalışan başına her hak ediş yılı bir satır (İş Kanunu md.53, kademeler +-- İnsan Kaynakları ayarlarından). YearNo = 0 satırı devir/düzeltmeleri ve ilk hak edişten önce +-- kullanılan izni toplar; böylece satırların toplamı bakiyeyi verir. +WITH TenantCfg AS ( + SELECT t."TenantId", + COALESCE(z.Days1To5, 14)::numeric(5, 1) AS Days1To5, + COALESCE(z.Days5To15, 20)::numeric(5, 1) AS Days5To15, + COALESCE(z.Days15Plus, 26)::numeric(5, 1) AS Days15Plus, + COALESCE(z.MinDaysYoungOld, 20)::numeric(5, 1) AS MinDaysYoungOld + FROM (SELECT DISTINCT "TenantId" FROM "AbpUsers" WHERE "IsDeleted" = FALSE) t + CROSS JOIN LATERAL ( + SELECT public."Hr_F_HrSetting"(t."TenantId", 'App.HrSettings.Leave.AnnualDays1To5') AS a, + public."Hr_F_HrSetting"(t."TenantId", 'App.HrSettings.Leave.AnnualDays5To15') AS b, + public."Hr_F_HrSetting"(t."TenantId", 'App.HrSettings.Leave.AnnualDays15Plus') AS c, + public."Hr_F_HrSetting"(t."TenantId", 'App.HrSettings.Leave.MinDaysYoungOld') AS d + ) s + CROSS JOIN LATERAL ( + SELECT CASE WHEN pg_input_is_valid(s.a, 'numeric(5,1)') THEN s.a::numeric(5, 1) END AS Days1To5, + CASE WHEN pg_input_is_valid(s.b, 'numeric(5,1)') THEN s.b::numeric(5, 1) END AS Days5To15, + CASE WHEN pg_input_is_valid(s.c, 'numeric(5,1)') THEN s.c::numeric(5, 1) END AS Days15Plus, + CASE WHEN pg_input_is_valid(s.d, 'numeric(5,1)') THEN s.d::numeric(5, 1) END AS MinDaysYoungOld + ) z +), +Emp AS ( + SELECT u."Id" AS EmployeeId, u."TenantId" AS TenantId, + COALESCE(u."SeniorityStartDate", u."HireDate")::date AS SeniorityStartDate, + u."BirthDate"::date AS BirthDate, + u."TerminationDate"::date AS TerminationDate, + c.Days1To5, c.Days5To15, c.Days15Plus, c.MinDaysYoungOld + FROM "AbpUsers" u + INNER JOIN TenantCfg c ON c."TenantId" IS NOT DISTINCT FROM u."TenantId" + WHERE u."IsDeleted" = FALSE +), +Years AS ( + SELECT e.*, n.YearNo, + (e.SeniorityStartDate + make_interval(years => n.YearNo))::date AS AccrualDate, + (e.SeniorityStartDate + make_interval(years => n.YearNo + 1))::date AS NextPeriodDate + FROM Emp e + CROSS JOIN generate_series(1, 50) n (YearNo) + WHERE e.SeniorityStartDate IS NOT NULL +), +Entitlement AS ( + SELECT y.*, + CASE + WHEN y.YearNo <= 5 THEN y.Days1To5 + WHEN y.YearNo < 15 THEN y.Days5To15 + ELSE y.Days15Plus + END AS BaseDays, + CASE + WHEN y.BirthDate IS NULL THEN NULL + ELSE (EXTRACT(YEAR FROM y.AccrualDate) - EXTRACT(YEAR FROM y.BirthDate))::int + - CASE WHEN (y.BirthDate + make_interval(years => (EXTRACT(YEAR FROM y.AccrualDate) - EXTRACT(YEAR FROM y.BirthDate))::int))::date + > y.AccrualDate THEN 1 ELSE 0 END + END AS AgeAtAccrual + FROM Years y +), +Accrued AS ( + SELECT e.EmployeeId, e.TenantId, e.YearNo, e.AccrualDate, e.NextPeriodDate, + CASE + WHEN (e.AgeAtAccrual <= 18 OR e.AgeAtAccrual >= 50) AND e.BaseDays < e.MinDaysYoungOld THEN e.MinDaysYoungOld + ELSE e.BaseDays + END AS EntitledDays, + CASE WHEN e.AccrualDate <= LOCALTIMESTAMP::date + AND (e.TerminationDate IS NULL OR e.AccrualDate <= e.TerminationDate) THEN 1 ELSE 0 END AS IsAccrued + FROM Entitlement e +), +NextAccrual AS ( + SELECT a.EmployeeId, MIN(a.AccrualDate) AS NextAccrualDate + FROM Accrued a + WHERE a.IsAccrued = 0 + GROUP BY a.EmployeeId +), +Requests AS ( + -- Yalnız yıllık izinden düşen türler; reddedilen talep bakiyeye girmez. + SELECT r."EmployeeId" AS EmployeeId, r."StartDate"::date AS StartDate, r."Days" AS Days, + CASE WHEN r."ApprovalStatus" = 'Onaylandı' THEN 1 ELSE 0 END AS IsApproved + FROM "Hr_T_LeaveRequest" r + INNER JOIN "Hr_T_LeaveType" t ON t."Id" = r."LeaveTypeId" + WHERE r."IsDeleted" = FALSE AND t."DeductsFromAnnualLeave" = TRUE + AND (r."ApprovalStatus" IS NULL OR r."ApprovalStatus" <> 'Reddedildi') +), +Ledger AS ( + SELECT a.EmployeeId, a.TenantId, a.YearNo, a.AccrualDate, a.NextPeriodDate - 1 AS PeriodEndDate, + a.EntitledDays::numeric(38, 1) AS EntitledDays, + COALESCE((SELECT SUM(r.Days) FROM Requests r WHERE r.EmployeeId = a.EmployeeId AND r.IsApproved = 1 + AND r.StartDate >= a.AccrualDate AND r.StartDate < a.NextPeriodDate), 0)::numeric(38, 1) AS UsedDays, + COALESCE((SELECT SUM(r.Days) FROM Requests r WHERE r.EmployeeId = a.EmployeeId AND r.IsApproved = 0 + AND r.StartDate >= a.AccrualDate AND r.StartDate < a.NextPeriodDate), 0)::numeric(38, 1) AS PendingDays + FROM Accrued a + WHERE a.IsAccrued = 1 + UNION ALL + SELECT e.EmployeeId, e.TenantId, 0, NULL::date, NULL::date, + COALESCE((SELECT SUM(x."Days") FROM "Hr_T_LeaveBalanceAdjustment" x + WHERE x."EmployeeId" = e.EmployeeId AND x."IsDeleted" = FALSE AND x."IsActive" = TRUE), 0)::numeric(38, 1), + COALESCE((SELECT SUM(r.Days) FROM Requests r WHERE r.EmployeeId = e.EmployeeId AND r.IsApproved = 1 + AND (e.SeniorityStartDate IS NULL OR r.StartDate < (e.SeniorityStartDate + INTERVAL '1 year')::date)), 0)::numeric(38, 1), + COALESCE((SELECT SUM(r.Days) FROM Requests r WHERE r.EmployeeId = e.EmployeeId AND r.IsApproved = 0 + AND (e.SeniorityStartDate IS NULL OR r.StartDate < (e.SeniorityStartDate + INTERVAL '1 year')::date)), 0)::numeric(38, 1) + FROM Emp e +) +-- Id'deki Guid metni SQL Server CONVERT çıktısıyla aynı kalsın diye büyük harf. +SELECT CONCAT(upper(l.EmployeeId::text), '-', l.YearNo)::varchar(49) AS "Id", + l.TenantId AS "TenantId", + l.EmployeeId AS "EmployeeId", + CONCAT(u."Name", ' ', u."Surname")::varchar(129) AS "EmployeeName", + u."DepartmentId", + u."JobPositionId", + l.YearNo AS "YearNo", + l.AccrualDate AS "AccrualDate", + l.PeriodEndDate AS "PeriodEndDate", + l.EntitledDays AS "EntitledDays", + l.UsedDays AS "UsedDays", + l.PendingDays AS "PendingDays", + (l.EntitledDays - l.UsedDays)::numeric(38, 1) AS "RemainingDays", + SUM(l.EntitledDays - l.UsedDays) OVER (PARTITION BY l.EmployeeId ORDER BY l.YearNo + ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)::numeric(38, 1) AS "CumulativeRemainingDays", + na.NextAccrualDate AS "NextAccrualDate", + (SELECT a.EntitledDays FROM Accrued a WHERE a.EmployeeId = l.EmployeeId AND a.AccrualDate = na.NextAccrualDate)::numeric(5, 1) AS "NextAccrualDays" +FROM Ledger l +INNER JOIN "AbpUsers" u ON u."Id" = l.EmployeeId +LEFT JOIN NextAccrual na ON na.EmployeeId = l.EmployeeId +WHERE l.YearNo > 0 OR l.EntitledDays <> 0 OR l.UsedDays <> 0 OR l.PendingDays <> 0; diff --git a/configs/seeds/postgres/object/Hr_V_LeaveSummary.sql b/configs/seeds/postgres/object/Hr_V_LeaveSummary.sql new file mode 100644 index 00000000..55976f25 --- /dev/null +++ b/configs/seeds/postgres/object/Hr_V_LeaveSummary.sql @@ -0,0 +1,32 @@ +CREATE OR REPLACE VIEW public."Hr_V_LeaveSummary" +AS +-- Çalışan başına yıllık izin bakiyesi; defterin (Hr_V_LeaveBalanceYear) toplamıdır. +-- Kalan = hak edilen + düzeltme − onaylı kullanılan; Kullanılabilir = kalan − onay bekleyen. +SELECT u."Id", + u."TenantId", + u."Id" AS "EmployeeId", + CONCAT(u."Name", ' ', u."Surname")::varchar(129) AS "EmployeeName", + u."DepartmentId", + u."JobPositionId", + COALESCE(u."SeniorityStartDate", u."HireDate")::date AS "SeniorityStartDate", + CASE + WHEN COALESCE(u."SeniorityStartDate", u."HireDate") IS NULL THEN NULL + ELSE (EXTRACT(YEAR FROM LOCALTIMESTAMP) - EXTRACT(YEAR FROM COALESCE(u."SeniorityStartDate", u."HireDate")))::int + - CASE WHEN COALESCE(u."SeniorityStartDate", u."HireDate") + + make_interval(years => (EXTRACT(YEAR FROM LOCALTIMESTAMP) - EXTRACT(YEAR FROM COALESCE(u."SeniorityStartDate", u."HireDate")))::int) + > LOCALTIMESTAMP THEN 1 ELSE 0 END + END AS "ServiceYears", + COALESCE(SUM(CASE WHEN l."YearNo" > 0 THEN l."EntitledDays" END), 0)::numeric(9, 1) AS "EntitledDays", + COALESCE(SUM(CASE WHEN l."YearNo" = 0 THEN l."EntitledDays" END), 0)::numeric(9, 1) AS "AdjustmentDays", + COALESCE(SUM(l."UsedDays"), 0)::numeric(9, 1) AS "UsedDays", + COALESCE(SUM(l."PendingDays"), 0)::numeric(9, 1) AS "PendingDays", + COALESCE(SUM(l."EntitledDays" - l."UsedDays"), 0)::numeric(9, 1) AS "RemainingDays", + COALESCE(SUM(l."EntitledDays" - l."UsedDays" - l."PendingDays"), 0)::numeric(9, 1) AS "AvailableDays", + MAX(l."NextAccrualDate") AS "NextAccrualDate", + MAX(l."NextAccrualDays")::numeric(5, 1) AS "NextAccrualDays", + u."IsActive" +FROM "AbpUsers" u +LEFT JOIN public."Hr_V_LeaveBalanceYear" l ON l."EmployeeId" = u."Id" +WHERE u."IsDeleted" = FALSE +GROUP BY u."Id", u."TenantId", u."Name", u."Surname", u."DepartmentId", u."JobPositionId", + u."SeniorityStartDate", u."HireDate", u."IsActive"; diff --git a/configs/seeds/postgres/object/Hr_V_TimesheetMonthly.sql b/configs/seeds/postgres/object/Hr_V_TimesheetMonthly.sql new file mode 100644 index 00000000..71203bff --- /dev/null +++ b/configs/seeds/postgres/object/Hr_V_TimesheetMonthly.sql @@ -0,0 +1,60 @@ +CREATE OR REPLACE VIEW public."Hr_V_TimesheetMonthly" +AS +-- Personel bazlı aylık puantaj: günlük puantajın çalışan × ay toplamı. Id günlük satırların +-- PeriodKey değeridir (alt form ilişkisi). Fazla mesai ücreti çarpanı ve yıllık sınır ayardan gelir; +-- yıllık fazla mesai ocaktan o aya kadarki birikimdir. Ücretsiz izin günü (bordroda eksik gün) izinli +-- günlerin ücretsiz türdeki kısmıdır; eğitim günü çalışılmış sayılır. +WITH m AS ( + SELECT t."PeriodKey" AS PeriodKey, t."TenantId" AS TenantId, t."EmployeeId" AS EmployeeId, + t."PeriodYear" AS PeriodYear, t."PeriodMonth" AS PeriodMonth, + SUM(CASE WHEN t."DayType" = 'Work' THEN 1 ELSE 0 END)::int AS WorkDays, + (SUM(t."NetMinutes") / 60.0)::numeric(9, 2) AS NetHours, + SUM(t."LateMinutes")::int AS LateMinutes, + SUM(CASE WHEN t."LateMinutes" > 0 THEN 1 ELSE 0 END)::int AS LateDays, + SUM(t."EarlyLeaveMinutes")::int AS EarlyLeaveMinutes, + SUM(CASE WHEN t."EarlyLeaveMinutes" > 0 THEN 1 ELSE 0 END)::int AS EarlyLeaveDays, + SUM(t."OvertimeHours")::numeric(9, 2) AS OvertimeHours, + SUM(CASE WHEN t."DayType" = 'WeeklyOff' THEN 1 ELSE 0 END)::int AS WeeklyOffDays, + SUM(CASE WHEN t."DayType" = 'Holiday' THEN 1 ELSE 0 END)::int AS HolidayDays, + SUM(CASE WHEN t."DayType" = 'Leave' THEN 1 ELSE 0 END)::int AS LeaveDays, + SUM(CASE WHEN t."DayType" = 'Leave' AND lt."IsPaid" = FALSE THEN 1 ELSE 0 END)::int AS UnpaidLeaveDays, + SUM(CASE WHEN t."DayType" = 'Training' THEN 1 ELSE 0 END)::int AS TrainingDays, + SUM(CASE WHEN t."DayType" = 'Sick' THEN 1 ELSE 0 END)::int AS SickDays, + SUM(CASE WHEN t."DayType" = 'Absent' THEN 1 ELSE 0 END)::int AS AbsentDays, + SUM(CASE WHEN t."IsIncomplete" = TRUE THEN 1 ELSE 0 END)::int AS IncompleteDays, + SUM(CASE WHEN t."IsAssumed" = TRUE THEN 1 ELSE 0 END)::int AS AssumedDays + FROM "Hr_T_Timesheet" t + LEFT JOIN "Hr_T_LeaveType" lt ON lt."Id" = t."LeaveTypeId" + WHERE t."IsDeleted" = FALSE + GROUP BY t."PeriodKey", t."TenantId", t."EmployeeId", t."PeriodYear", t."PeriodMonth" +) +SELECT m.PeriodKey AS "Id", m.TenantId AS "TenantId", m.EmployeeId AS "EmployeeId", u."DepartmentId", u."JobPositionId", + m.PeriodYear AS "PeriodYear", m.PeriodMonth AS "PeriodMonth", COALESCE(p."Status", 'Open')::varchar(16) AS "PeriodStatus", + m.WorkDays AS "WorkDays", m.NetHours AS "NetHours", m.LateMinutes AS "LateMinutes", m.LateDays AS "LateDays", + m.EarlyLeaveMinutes AS "EarlyLeaveMinutes", m.EarlyLeaveDays AS "EarlyLeaveDays", + m.OvertimeHours AS "OvertimeHours", + (m.OvertimeHours * s.Rate)::numeric(9, 2) AS "OvertimePaidHours", + m.WeeklyOffDays AS "WeeklyOffDays", m.HolidayDays AS "HolidayDays", m.LeaveDays AS "LeaveDays", + m.UnpaidLeaveDays AS "UnpaidLeaveDays", m.TrainingDays AS "TrainingDays", m.SickDays AS "SickDays", + m.AbsentDays AS "AbsentDays", m.IncompleteDays AS "IncompleteDays", m.AssumedDays AS "AssumedDays", + (SUM(m.OvertimeHours) OVER (PARTITION BY m.TenantId, m.EmployeeId, m.PeriodYear ORDER BY m.PeriodMonth + ROWS UNBOUNDED PRECEDING))::numeric(9, 2) AS "YearOvertimeHours", + s.AnnualLimit AS "AnnualOvertimeLimit", + CASE WHEN SUM(m.OvertimeHours) OVER (PARTITION BY m.TenantId, m.EmployeeId, m.PeriodYear ORDER BY m.PeriodMonth + ROWS UNBOUNDED PRECEDING) > s.AnnualLimit THEN 1 ELSE 0 END AS "IsOverAnnualLimit" +FROM m +INNER JOIN "AbpUsers" u ON u."Id" = m.EmployeeId +LEFT JOIN LATERAL ( + SELECT x."Status" FROM "Hr_T_TimesheetPeriod" x + WHERE x."IsDeleted" = FALSE AND x."PeriodYear" = m.PeriodYear AND x."PeriodMonth" = m.PeriodMonth + AND x."TenantId" IS NOT DISTINCT FROM m.TenantId + LIMIT 1 +) p ON TRUE +CROSS JOIN LATERAL ( + SELECT replace(public."Hr_F_HrSetting"(m.TenantId, 'App.HrSettings.Attendance.OvertimeRate'), ',', '.') AS RateText, + replace(public."Hr_F_HrSetting"(m.TenantId, 'App.HrSettings.Attendance.AnnualOvertimeLimitHours'), ',', '.') AS LimitText +) h +CROSS JOIN LATERAL ( + SELECT COALESCE(CASE WHEN pg_input_is_valid(h.RateText, 'numeric(9,2)') THEN h.RateText::numeric(9, 2) END, 1.5)::numeric(9, 2) AS Rate, + COALESCE(CASE WHEN pg_input_is_valid(h.LimitText, 'numeric(9,2)') THEN h.LimitText::numeric(9, 2) END, 270)::numeric(9, 2) AS AnnualLimit +) s; diff --git a/configs/seeds/postgres/object/Prc_T_DeliveryMethod.sql b/configs/seeds/postgres/object/Prc_T_DeliveryMethod.sql new file mode 100644 index 00000000..72b30f77 --- /dev/null +++ b/configs/seeds/postgres/object/Prc_T_DeliveryMethod.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS public."Prc_T_DeliveryMethod" +( + "Id" uuid NOT NULL DEFAULT gen_random_uuid(), + "TenantId" uuid NULL, + "Name" varchar(128) 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_Prc_T_DeliveryMethod" PRIMARY KEY ("Id") +); diff --git a/configs/seeds/postgres/object/Prc_T_Incoterm.sql b/configs/seeds/postgres/object/Prc_T_Incoterm.sql new file mode 100644 index 00000000..80e925f6 --- /dev/null +++ b/configs/seeds/postgres/object/Prc_T_Incoterm.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS public."Prc_T_Incoterm" +( + "Id" uuid NOT NULL DEFAULT gen_random_uuid(), + "TenantId" uuid NULL, + "Code" varchar(8) NOT NULL, + "Name" varchar(128) 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_Prc_T_Incoterm" PRIMARY KEY ("Id") +); diff --git a/configs/seeds/postgres/object/Prc_T_PaymentMethod.sql b/configs/seeds/postgres/object/Prc_T_PaymentMethod.sql new file mode 100644 index 00000000..a7dde8e0 --- /dev/null +++ b/configs/seeds/postgres/object/Prc_T_PaymentMethod.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS public."Prc_T_PaymentMethod" +( + "Id" uuid NOT NULL DEFAULT gen_random_uuid(), + "TenantId" uuid NULL, + "Name" varchar(128) NOT NULL, + "Journal" varchar(64) 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_Prc_T_PaymentMethod" PRIMARY KEY ("Id") +); diff --git a/configs/seeds/postgres/object/Prc_T_PaymentTerm.sql b/configs/seeds/postgres/object/Prc_T_PaymentTerm.sql new file mode 100644 index 00000000..62e05ea4 --- /dev/null +++ b/configs/seeds/postgres/object/Prc_T_PaymentTerm.sql @@ -0,0 +1,17 @@ +CREATE TABLE IF NOT EXISTS public."Prc_T_PaymentTerm" +( + "Id" uuid NOT NULL DEFAULT gen_random_uuid(), + "TenantId" uuid NULL, + "Name" varchar(128) NOT NULL, + "DueDays" integer 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_Prc_T_PaymentTerm" PRIMARY KEY ("Id") +); diff --git a/configs/seeds/postgres/object/Prj_T_Customer.sql b/configs/seeds/postgres/object/Prj_T_Customer.sql deleted file mode 100644 index c792f3a0..00000000 --- a/configs/seeds/postgres/object/Prj_T_Customer.sql +++ /dev/null @@ -1,18 +0,0 @@ --- Create Table -CREATE TABLE IF NOT EXISTS dbo."Prj_T_Customer" ( - "Id" UUID NOT NULL CONSTRAINT "PK_Prj_T_Customer" PRIMARY KEY, - "TenantId" UUID NULL, - "Name" VARCHAR(300) NOT NULL, - "ContactName" VARCHAR(200) NULL, - "Email" VARCHAR(200) NULL, - "Phone" VARCHAR(50) 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 -); - -CREATE INDEX IF NOT EXISTS "IX_Prj_T_Customer_TenantId_Name" ON dbo."Prj_T_Customer" ("TenantId" ASC, "Name" ASC); diff --git a/configs/seeds/postgres/object/Prj_T_Milestone.sql b/configs/seeds/postgres/object/Prj_T_Milestone.sql deleted file mode 100644 index b5ebbea2..00000000 --- a/configs/seeds/postgres/object/Prj_T_Milestone.sql +++ /dev/null @@ -1,17 +0,0 @@ --- Create Table -CREATE TABLE IF NOT EXISTS dbo."Prj_T_Milestone" ( - "Id" UUID NOT NULL CONSTRAINT "PK_Prj_T_Milestone" PRIMARY KEY, - "TenantId" UUID NULL, - "Name" VARCHAR(300) NOT NULL, - "TargetDate" TIMESTAMP NULL, - "IsCompleted" 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 -); - -CREATE INDEX IF NOT EXISTS "IX_Prj_T_Milestone_TenantId_Name" ON dbo."Prj_T_Milestone" ("TenantId" ASC, "Name" ASC); diff --git a/configs/seeds/postgres/object/Prj_T_Tag.sql b/configs/seeds/postgres/object/Prj_T_Tag.sql deleted file mode 100644 index 16d08f64..00000000 --- a/configs/seeds/postgres/object/Prj_T_Tag.sql +++ /dev/null @@ -1,16 +0,0 @@ --- Create Table -CREATE TABLE IF NOT EXISTS dbo."Prj_T_Tag" ( - "Id" UUID NOT NULL CONSTRAINT "PK_Prj_T_Tag" PRIMARY KEY, - "TenantId" UUID NULL, - "Name" VARCHAR(200) NOT NULL, - "ColorCode" VARCHAR(20) 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 -); - -CREATE INDEX IF NOT EXISTS "IX_Prj_T_Tag_TenantId_Name" ON dbo."Prj_T_Tag" ("TenantId" ASC, "Name" ASC); diff --git a/configs/seeds/postgres/object/Prj_T_Task.sql b/configs/seeds/postgres/object/Prj_T_Task.sql deleted file mode 100644 index deab8c32..00000000 --- a/configs/seeds/postgres/object/Prj_T_Task.sql +++ /dev/null @@ -1,30 +0,0 @@ --- Create Table -CREATE TABLE IF NOT EXISTS dbo."Prj_T_Task" ( - "Id" UUID NOT NULL CONSTRAINT "PK_Prj_T_Task" PRIMARY KEY, - "TenantId" UUID NULL, - "Name" VARCHAR(300) NOT NULL, - "MilestoneId" UUID NULL REFERENCES dbo."Prj_T_Milestone" ("Id"), - "AssigneeIds" VARCHAR(1000) NULL, - "TagIds" VARCHAR(1000) NULL, - "CustomerId" UUID NULL REFERENCES dbo."Prj_T_Customer" ("Id"), - "CustomerContact" VARCHAR(200) NULL, - "SalesOrderLine" VARCHAR(300) NULL, - "PlannedHours" NUMERIC(18, 2) NULL, - "ProgressPercent" NUMERIC(18, 2) NULL, - "Status" VARCHAR(50) NOT NULL DEFAULT 'Taslak', - "PlannedStartDate" TIMESTAMP NULL, - "PlannedEndDate" TIMESTAMP NULL, - "IsFavorite" BOOLEAN NOT NULL DEFAULT FALSE, - "ParentTaskId" UUID NULL REFERENCES dbo."Prj_T_Task" ("Id"), - "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 -); - -CREATE INDEX IF NOT EXISTS "IX_Prj_T_Task_TenantId_Status" ON dbo."Prj_T_Task" ("TenantId" ASC, "Status" ASC); -CREATE INDEX IF NOT EXISTS "IX_Prj_T_Task_ParentTaskId" ON dbo."Prj_T_Task" ("ParentTaskId" ASC); diff --git a/configs/seeds/postgres/object/Prj_T_TaskDependency.sql b/configs/seeds/postgres/object/Prj_T_TaskDependency.sql deleted file mode 100644 index eb947c67..00000000 --- a/configs/seeds/postgres/object/Prj_T_TaskDependency.sql +++ /dev/null @@ -1,17 +0,0 @@ --- Create Table -CREATE TABLE IF NOT EXISTS dbo."Prj_T_TaskDependency" ( - "Id" UUID NOT NULL CONSTRAINT "PK_Prj_T_TaskDependency" PRIMARY KEY, - "TenantId" UUID NULL, - "TaskId" UUID NOT NULL REFERENCES dbo."Prj_T_Task" ("Id"), - "DependsOnTaskId" UUID NOT NULL REFERENCES dbo."Prj_T_Task" ("Id"), - "Note" VARCHAR(300) 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 -); - -CREATE INDEX IF NOT EXISTS "IX_Prj_T_TaskDependency_TaskId" ON dbo."Prj_T_TaskDependency" ("TaskId" ASC); diff --git a/configs/seeds/postgres/object/Prj_T_Timesheet.sql b/configs/seeds/postgres/object/Prj_T_Timesheet.sql deleted file mode 100644 index f9db6efe..00000000 --- a/configs/seeds/postgres/object/Prj_T_Timesheet.sql +++ /dev/null @@ -1,19 +0,0 @@ --- Create Table -CREATE TABLE IF NOT EXISTS dbo."Prj_T_Timesheet" ( - "Id" UUID NOT NULL CONSTRAINT "PK_Prj_T_Timesheet" PRIMARY KEY, - "TenantId" UUID NULL, - "TaskId" UUID NOT NULL REFERENCES dbo."Prj_T_Task" ("Id"), - "WorkDate" TIMESTAMP NOT NULL, - "UserId" UUID NULL, - "Description" VARCHAR(500) NULL, - "Hours" NUMERIC(18, 2) 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 -); - -CREATE INDEX IF NOT EXISTS "IX_Prj_T_Timesheet_TaskId_WorkDate" ON dbo."Prj_T_Timesheet" ("TaskId" ASC, "WorkDate" DESC); diff --git a/configs/seeds/postgres/object/Sas_H_ListFormFieldsDictionaryJson.sql b/configs/seeds/postgres/object/Sas_H_ListFormFieldsDictionaryJson.sql index ec605be1..16e611ef 100644 --- a/configs/seeds/postgres/object/Sas_H_ListFormFieldsDictionaryJson.sql +++ b/configs/seeds/postgres/object/Sas_H_ListFormFieldsDictionaryJson.sql @@ -1,18 +1,231 @@ -CREATE OR REPLACE FUNCTION dbo."Sas_H_ListFormFieldsDictionaryJson" +-- SQL Server surumu sonuc kumesi donduren bir prosedurdur; PostgreSQL'de prosedur satir +-- donduremedigi icin ayni tek kolonlu sonucu donduren fonksiyondur. +CREATE OR REPLACE FUNCTION public."Sas_H_ListFormFieldsDictionaryJson" ( - 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_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 ) -RETURNS JSONB +RETURNS TABLE ("ListFormDataDictionaryJson" text) LANGUAGE plpgsql AS $$ -DECLARE - v_result JSONB; BEGIN + -- Metin ve kolon tipi her ekran ve alan icin aranir. Dil metninin indeksi (ResourceName, Key) + -- Key ile baslamadigi icin dogrudan arama her alanda kulturun butun metinlerini tarar; ikisi + -- bir kez anahtarli tabloya alinir ve aramalar seek olur. + -- SQL Server karsilastirmasi buyuk/kucuk harf duyarsizdir; anahtarlar kucuk harfle tutulur. + IF to_regclass('pg_temp."ListFormDictText"') IS NOT NULL THEN + DROP TABLE pg_temp."ListFormDictText"; + END IF; + CREATE TEMP TABLE "ListFormDictText" + ( + "Key" text NOT NULL PRIMARY KEY, + "Value" text NULL + ) ON COMMIT DROP; + INSERT INTO "ListFormDictText" ("Key", "Value") + SELECT DISTINCT ON (lower(lt."Key" COLLATE "default")) + lower(lt."Key" COLLATE "default"), + lt."Value" + FROM public."Sas_H_LanguageText" lt + WHERE lt."CultureName" = p_UiCultureName + AND lt."Key" IS NOT NULL + AND (p_ResourceName IS NULL OR lt."ResourceName" = p_ResourceName) + ORDER BY lower(lt."Key" COLLATE "default"), lt."ResourceName" NULLS FIRST; + + IF to_regclass('pg_temp."ListFormDictColumn"') IS NOT NULL THEN + DROP TABLE pg_temp."ListFormDictColumn"; + END IF; + CREATE TEMP TABLE "ListFormDictColumn" + ( + "TableName" text NOT NULL, + "ColumnName" text NOT NULL, + "SqlDataType" text NULL, + PRIMARY KEY ("TableName", "ColumnName") + ) ON COMMIT DROP; + + INSERT INTO "ListFormDictColumn" ("TableName", "ColumnName", "SqlDataType") + SELECT DISTINCT ON (lower(r.relname::text COLLATE "default"), lower(a.attname::text COLLATE "default")) + lower(r.relname::text COLLATE "default"), + lower(a.attname::text COLLATE "default"), + format_type(a.atttypid, NULL) + FROM pg_catalog.pg_namespace n + INNER JOIN pg_catalog.pg_class r ON r.relnamespace = n.oid + INNER JOIN pg_catalog.pg_attribute a ON a.attrelid = r.oid + WHERE n.nspname = 'public' + AND r.relkind IN ('r', 'p') + AND a.attnum > 0 + AND NOT a.attisdropped + ORDER BY lower(r.relname::text COLLATE "default"), lower(a.attname::text COLLATE "default"), r.relname, a.attname; + + -- Lookup ayristirmasi alan basina bir kez yapilir. Ic ice APPLY zinciri sorguda ifade olarak + -- acilir; her basvuruda REPLACE/JSON_VALUE zinciri yeniden hesaplanir ve sozluk dakikalar surer. + -- "JsonScalars" JSON_VALUE karsiligidir: nesnenin ust seviyedeki skaler degerleri metin olarak; + -- nesne/dizi degeri ve gecersiz JSON NULL verir. + IF to_regclass('pg_temp."ListFormDictLookup"') IS NOT NULL THEN + DROP TABLE pg_temp."ListFormDictLookup"; + END IF; + CREATE TEMP TABLE "ListFormDictLookup" + ( + "FieldId" uuid NOT NULL PRIMARY KEY, + "LookupJsonTextDecoded" text NULL, + "LookupJson" jsonb NULL, + "JsonScalars" jsonb NULL, + "LenientScalars" jsonb NULL, + "CleanLookupQuery" text NULL, + "ParsedLookupTableName" text NULL, + "ParsedLookupValueColumn" text NULL, + "ParsedLookupDisplayColumn" text NULL + ) ON COMMIT DROP; + + INSERT INTO "ListFormDictLookup" ("FieldId", "LookupJsonTextDecoded") + SELECT x."Id", x."LookupJsonTextDecoded" + FROM + ( + SELECT + f."Id", + NULLIF(BTRIM( + REPLACE( + REPLACE( + REPLACE( + REPLACE( + REPLACE( + NULLIF(BTRIM(f."LookupJson"::text), ''), + '\\u0022', '"'), + '\u0022', '"'), + '\\u0027', ''''), + '\u0027', ''''), + '"', '"') + ), '') AS "LookupJsonTextDecoded" + FROM public."Sas_H_ListFormField" f + ) x + WHERE x."LookupJsonTextDecoded" IS NOT NULL; + + UPDATE "ListFormDictLookup" lk + SET "LookupJson" = lk."LookupJsonTextDecoded"::jsonb + WHERE lk."LookupJsonTextDecoded" IS JSON OBJECT + OR lk."LookupJsonTextDecoded" IS JSON ARRAY; + + UPDATE "ListFormDictLookup" lk + SET "JsonScalars" = COALESCE( + ( + SELECT jsonb_object_agg(e.key, e.value #>> '{}') + FROM jsonb_each(lk."LookupJson") e + WHERE jsonb_typeof(e.value) IN ('string', 'number', 'boolean') + ), + '{}'::jsonb) + WHERE jsonb_typeof(lk."LookupJson") = 'object'; + + -- SQL Server JSON_VALUE metni akis halinde okur: gecersiz JSON'da aranan anahtar bozuk kisimdan + -- once geliyorsa degeri dondurur (LookupQuery icinde kacissiz tirnak kalan lookup'larda + -- ValueExpr/DisplayExpr boyle okunur). ISJSON korumasi olmayan ayristirma bu kumeyi kullanir; + -- anahtarin ilk gorundugu deger alinir. + UPDATE "ListFormDictLookup" lk + SET "LenientScalars" = CASE + WHEN lk."LookupJson" IS NOT NULL THEN lk."JsonScalars" + WHEN lk."LookupJsonTextDecoded" LIKE '{%' THEN + ( + SELECT jsonb_object_agg(p."Key", p."Value") + FROM + ( + SELECT DISTINCT ON (r.m[1]) + r.m[1] AS "Key", + r.m[2]::jsonb #>> '{}' AS "Value" + FROM regexp_matches( + lk."LookupJsonTextDecoded", + '"([^"\\]*)"\s*:\s*("(?:[^"\\]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*"|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null)', + 'g') WITH ORDINALITY AS r(m, n) + ORDER BY r.m[1], r.n + ) p + ) + END; + + UPDATE "ListFormDictLookup" lk + SET "CleanLookupQuery" = NULLIF(BTRIM( + REPLACE( + REPLACE( + REPLACE( + REPLACE( + REPLACE( + REPLACE( + REPLACE( + REPLACE( + lk."JsonScalars" ->> 'LookupQuery', + '"', ''), + '[', ''), + ']', ''), + '`', ''), + chr(13), ' '), + chr(10), ' '), + chr(9), ' '), + ' ', ' ') + ), ''); + + UPDATE "ListFormDictLookup" lk + SET "ParsedLookupTableName" = CASE + WHEN lqs."FromPos" > 0 AND lqs."StopPos" > lqs."FromPos" + THEN NULLIF(regexp_replace(left( + BTRIM(substring( + lk."CleanLookupQuery" + FROM lqs."FromPos" + 6 + FOR lqs."StopPos" - (lqs."FromPos" + 6) + )), + strpos(BTRIM(substring( + lk."CleanLookupQuery" + FROM lqs."FromPos" + 6 + FOR lqs."StopPos" - (lqs."FromPos" + 6) + )) || ' ', ' ') - 1 + ), 'dbo\.', '', 'gi'), '') + END, + "ParsedLookupValueColumn" = COALESCE( + NULLIF(lk."LenientScalars" ->> 'ValueExpr', ''), + NULLIF(lk."LenientScalars" ->> 'valueExpr', ''), + NULLIF(lk."LenientScalars" ->> 'valueField', ''), + NULLIF(lk."LenientScalars" ->> 'valueColumn', ''), + NULLIF(lk."LenientScalars" ->> 'keyField', ''), + NULLIF(lk."LenientScalars" ->> 'keyFieldName', '') + ), + "ParsedLookupDisplayColumn" = COALESCE( + NULLIF(lk."LenientScalars" ->> 'DisplayExpr', ''), + NULLIF(lk."LenientScalars" ->> 'displayExpr', ''), + NULLIF(lk."LenientScalars" ->> 'displayField', ''), + NULLIF(lk."LenientScalars" ->> 'displayColumn', ''), + NULLIF(lk."LenientScalars" ->> 'textField', ''), + NULLIF(lk."LenientScalars" ->> 'textColumn', ''), + NULLIF(lk."LenientScalars" ->> 'nameField', ''), + NULLIF(lk."LenientScalars" ->> 'nameColumn', '') + ) + FROM + ( + SELECT + lqp."FieldId", + lqp."FromPos", + CASE + WHEN lqp."FromPos" <= 0 THEN 0 + WHEN lqp."WherePos" > lqp."FromPos" THEN lqp."WherePos" + WHEN lqp."GroupPos" > lqp."FromPos" THEN lqp."GroupPos" + WHEN lqp."OrderPos" > lqp."FromPos" THEN lqp."OrderPos" + ELSE length(lqp."CleanLookupQuery") + 1 + END AS "StopPos" + FROM + ( + SELECT + l2."FieldId", + l2."CleanLookupQuery", + strpos(upper(COALESCE(l2."CleanLookupQuery", '')), ' FROM ') AS "FromPos", + strpos(upper(COALESCE(l2."CleanLookupQuery", '')), ' ORDER BY ') AS "OrderPos", + strpos(upper(COALESCE(l2."CleanLookupQuery", '')), ' WHERE ') AS "WherePos", + strpos(upper(COALESCE(l2."CleanLookupQuery", '')), ' GROUP BY ') AS "GroupPos" + FROM "ListFormDictLookup" l2 + ) lqp + ) lqs + WHERE lqs."FieldId" = lk."FieldId"; + + -- FOR JSON PATH NULL degerli ozellikleri yazmaz ve satir yoksa NULL dondurur; json_strip_nulls + -- ve COUNT kontrolu ayni sonucu verir. json (jsonb degil) ozellik sirasini korur. + RETURN QUERY WITH list_form_source AS ( SELECT @@ -20,898 +233,207 @@ BEGIN l."ListFormCode", l."CultureName", l."Name", - - lt_name."Value" AS name_value, - + lt_name."Value" AS "NameValue", l."Title", - lt_title."Value" AS title_value, - + lt_title."Value" AS "TitleValue", l."Description", - lt_desc."Value" AS description_value, - + lt_desc."Value" AS "DescriptionValue", l."TableName", - - NULLIF( - BTRIM(l."SelectCommand"::TEXT), - '' - ) AS select_command_text, - + NULLIF(BTRIM(l."SelectCommand"::text), '') AS "SelectCommandText", l."KeyFieldName", l."UserId", l."RoleId" - - FROM dbo."Sas_H_ListForm" l - - /* - SQL Server OUTER APPLY TOP (1) - PostgreSQL LEFT JOIN LATERAL ... LIMIT 1 - */ - LEFT JOIN LATERAL - ( - SELECT lt."Value" - FROM dbo."Sas_H_LanguageText" lt - WHERE lt."CultureName" = p_ui_culture_name - AND lt."Key" = l."Name" - AND - ( - p_resource_name IS NULL - OR lt."ResourceName" = p_resource_name - ) - ORDER BY lt."ResourceName" - LIMIT 1 - ) lt_name ON TRUE - - LEFT JOIN LATERAL - ( - SELECT lt."Value" - FROM dbo."Sas_H_LanguageText" lt - WHERE lt."CultureName" = p_ui_culture_name - AND lt."Key" = l."Title" - AND - ( - p_resource_name IS NULL - OR lt."ResourceName" = p_resource_name - ) - ORDER BY lt."ResourceName" - LIMIT 1 - ) lt_title ON TRUE - - LEFT JOIN LATERAL - ( - SELECT lt."Value" - FROM dbo."Sas_H_LanguageText" lt - WHERE lt."CultureName" = p_ui_culture_name - AND lt."Key" = l."Description" - AND - ( - p_resource_name IS NULL - OR lt."ResourceName" = p_resource_name - ) - ORDER BY lt."ResourceName" - LIMIT 1 - ) lt_desc ON TRUE - + FROM public."Sas_H_ListForm" l + LEFT JOIN "ListFormDictText" lt_name ON lt_name."Key" = lower(l."Name" COLLATE "default") + LEFT JOIN "ListFormDictText" lt_title ON lt_title."Key" = lower(l."Title" COLLATE "default") + LEFT JOIN "ListFormDictText" lt_desc ON lt_desc."Key" = lower(l."Description" COLLATE "default") WHERE - ( - p_definition_culture_name IS NULL - OR l."CultureName" = p_definition_culture_name - ) - + (p_DefinitionCultureName IS NULL OR l."CultureName" = p_DefinitionCultureName) + AND (p_ListFormCode IS NULL OR l."ListFormCode" = p_ListFormCode) AND ( - p_list_form_code IS NULL - OR l."ListFormCode" = p_list_form_code + p_OnlyBaseDefinitions = FALSE + OR (l."UserId" IS NULL AND l."RoleId" IS NULL) ) + ) + SELECT CASE WHEN COUNT(*) > 0 THEN json_strip_nulls( + json_build_object( + 'listForms', + json_agg( + json_build_object( + 'listFormCode', l."ListFormCode", - AND - ( - p_only_base_definitions = FALSE + 'displayName', COALESCE(l."NameValue"::text, COALESCE(l."Name", '')), + 'title', COALESCE(l."TitleValue"::text, COALESCE(l."Title", '')), + 'description', COALESCE(l."DescriptionValue"::text, COALESCE(l."Description", '')), - OR - ( - l."UserId" IS NULL - AND l."RoleId" IS NULL - ) - ) - ), + 'tableName', COALESCE(l."TableName", ''), + 'sqlSourceName', COALESCE(COALESCE(NULLIF(l."TableName", ''), l."SelectCommandText"), ''), + 'sourceKind', CASE + WHEN COALESCE(NULLIF(l."TableName", ''), l."SelectCommandText") IS NULL THEN 'unknown' + WHEN upper(ltrim(COALESCE(NULLIF(l."TableName", ''), l."SelectCommandText"))) LIKE 'SELECT%' THEN 'selectSql' + ELSE 'table' + END, - list_forms_json AS - ( - SELECT - jsonb_build_object - ( - 'listFormCode', - l."ListFormCode", + 'keyFieldName', COALESCE(l."KeyFieldName", ''), - 'displayName', - COALESCE( - l.name_value::TEXT, - l."Name", - '' - ), - - 'title', - COALESCE( - l.title_value::TEXT, - l."Title", - '' - ), - - 'description', - COALESCE( - l.description_value::TEXT, - l."Description", - '' - ), - - 'tableName', - COALESCE( - l."TableName", - '' - ), - - 'sqlSourceName', - COALESCE( - NULLIF( + 'searchText', concat_ws(' ', + l."ListFormCode", l."TableName", - '' - ), - l.select_command_text, - '' - ), - - 'sourceKind', - CASE - - WHEN COALESCE( - NULLIF( - l."TableName", - '' - ), - l.select_command_text - ) IS NULL - THEN 'unknown' - - WHEN UPPER( - BTRIM( - COALESCE( - NULLIF( - l."TableName", - '' - ), - l.select_command_text - ) - ) - ) LIKE 'SELECT%' - THEN 'selectSql' - - ELSE 'table' - - END, - - 'keyFieldName', - COALESCE( - l."KeyFieldName", - '' - ), - - 'searchText', - CONCAT_WS( - ' ', - l."ListFormCode", - l."TableName", - l.select_command_text, - - COALESCE( - l.name_value::TEXT, - l."Name" + l."SelectCommandText", + COALESCE(l."NameValue"::text, l."Name"), + COALESCE(l."TitleValue"::text, l."Title"), + COALESCE(l."DescriptionValue"::text, l."Description") ), - COALESCE( - l.title_value::TEXT, - l."Title" - ), - - COALESCE( - l.description_value::TEXT, - l."Description" - ) - ), - - 'fields', - COALESCE - ( + 'fields', ( - SELECT - jsonb_agg - ( - jsonb_build_object - ( - 'field', - f."FieldName", + SELECT json_agg(fj."FieldJson" ORDER BY fj."SortNo", fj."FieldName") + FROM + ( + SELECT + COALESCE(f."ListOrderNo", 999999) AS "SortNo", + f."FieldName", + json_build_object( + 'field', f."FieldName", + 'displayName', COALESCE(lt_cap."Value"::text, COALESCE(f."CaptionName", '')), - 'displayName', - COALESCE( - lt_cap."Value"::TEXT, - f."CaptionName", - '' - ), + 'sqlDataType', sc."SqlDataType", + 'listOrderNo', f."ListOrderNo", + 'isVisible', COALESCE(f."Visible", TRUE), - 'sqlDataType', - sc.sql_data_type, - - 'listOrderNo', - f."ListOrderNo", - - 'isVisible', - COALESCE( - f."Visible", - TRUE - ), - - 'isBooleanColumn', - CASE - - WHEN sc.sql_data_type = 'boolean' - THEN TRUE - - WHEN f."FieldName" LIKE 'Is%' - THEN TRUE - - WHEN f."FieldName" LIKE 'Has%' - THEN TRUE - - WHEN f."FieldName" IN + 'isBooleanColumn', CASE + WHEN sc."SqlDataType" = 'boolean' THEN TRUE + WHEN fn."Fn" LIKE 'is%' THEN TRUE + WHEN fn."Fn" LIKE 'has%' THEN TRUE + WHEN fn."Fn" IN ( - 'Monday', - 'Tuesday', - 'Wednesday', - 'Thursday', - 'Friday', - 'Saturday', - 'Sunday' - ) - THEN TRUE - - WHEN f."FieldName" IN + 'monday', 'tuesday', 'wednesday', 'thursday', + 'friday', 'saturday', 'sunday' + ) THEN TRUE + WHEN fn."Fn" IN ( - 'EmailConfirmed', - 'TwoFactorEnabled', - 'IsVerified', - 'IsDefault', - 'IsPublic', - 'IsEnabled', - 'IsPublished', - 'IsPinned', - 'IsAnonymous', - 'IsFixed', - 'IsCustomized', - 'IsEncrypted', - 'IsInherited', - 'ZipRequired', - 'StateRequired' - ) - THEN TRUE - - ELSE FALSE - - END, - - 'lookupJsonText', - lj.lookup_json_text_decoded, - - 'lookupTableName', - COALESCE - ( - CASE - WHEN lj.lookup_json IS NOT NULL - THEN COALESCE - ( - lj.lookup_json ->> 'tableName', - lj.lookup_json ->> 'sqlTableName', - lj.lookup_json ->> 'lookupTableName', - lj.lookup_json #>> '{dataSource,tableName}', - lj.lookup_json #>> '{dataSource,sqlTableName}' - ) - END, - - lkp.parsed_lookup_table_name - ), - - 'lookupValueColumn', - COALESCE - ( - CASE - WHEN lj.lookup_json IS NOT NULL - THEN COALESCE - ( - lj.lookup_json ->> 'valueExpr', - lj.lookup_json ->> 'valueField', - lj.lookup_json ->> 'valueColumn', - lj.lookup_json ->> 'keyField', - lj.lookup_json ->> 'keyFieldName' - ) - END, - - lkp.parsed_lookup_value_column - ), - - 'lookupDisplayColumn', - COALESCE - ( - CASE - WHEN lj.lookup_json IS NOT NULL - THEN COALESCE - ( - lj.lookup_json ->> 'displayExpr', - lj.lookup_json ->> 'displayField', - lj.lookup_json ->> 'displayColumn', - lj.lookup_json ->> 'textField', - lj.lookup_json ->> 'textColumn', - lj.lookup_json ->> 'nameField', - lj.lookup_json ->> 'nameColumn' - ) - END, - - lkp.parsed_lookup_display_column - ), - - 'lookupJoinMode', - CASE - - WHEN lj.lookup_json_text_decoded IS NOT NULL - AND f."FieldName" NOT LIKE '%Id' - AND COALESCE( - lkp.parsed_lookup_value_column, - '' - ) IN - ( - 'Name', - 'Code', - 'Key', - 'Value', - 'key', - 'value' - ) - THEN 'direct-string' - - WHEN lj.lookup_json_text_decoded IS NOT NULL - AND f."FieldName" NOT LIKE '%Id' - THEN 'direct-string' - - WHEN lj.lookup_json_text_decoded IS NOT NULL - OR f."FieldName" LIKE '%Id' - THEN 'convert-nvarchar-safe' - - ELSE NULL - - END, - - 'lookupFallbackToSourceColumn', - CASE - WHEN - lj.lookup_json_text_decoded IS NOT NULL - AND f."FieldName" NOT LIKE '%Id' - THEN TRUE - + 'emailconfirmed', 'twofactorenabled', 'isverified', + 'isdefault', 'ispublic', 'isenabled', 'ispublished', + 'ispinned', 'isanonymous', 'isfixed', 'iscustomized', + 'isencrypted', 'isinherited', 'ziprequired', 'staterequired' + ) THEN TRUE ELSE FALSE END, - 'lookupSearchText', - CASE - WHEN lj.lookup_json_text_decoded IS NOT NULL - THEN CONCAT_WS - ( - ' ', + 'lookupJsonText', lk."LookupJsonTextDecoded", + + 'lookupTableName', COALESCE( + COALESCE( + lk."JsonScalars" ->> 'tableName', + lk."JsonScalars" ->> 'sqlTableName', + lk."JsonScalars" ->> 'lookupTableName', + CASE WHEN jsonb_typeof(lk."LookupJson" #> '{dataSource,tableName}') IN ('string', 'number', 'boolean') + THEN lk."LookupJson" #>> '{dataSource,tableName}' END, + CASE WHEN jsonb_typeof(lk."LookupJson" #> '{dataSource,sqlTableName}') IN ('string', 'number', 'boolean') + THEN lk."LookupJson" #>> '{dataSource,sqlTableName}' END + ), + lk."ParsedLookupTableName" + ), + + 'lookupValueColumn', COALESCE( + COALESCE( + lk."JsonScalars" ->> 'valueExpr', + lk."JsonScalars" ->> 'valueField', + lk."JsonScalars" ->> 'valueColumn', + lk."JsonScalars" ->> 'keyField', + lk."JsonScalars" ->> 'keyFieldName' + ), + lk."ParsedLookupValueColumn" + ), + + 'lookupDisplayColumn', COALESCE( + COALESCE( + lk."JsonScalars" ->> 'displayExpr', + lk."JsonScalars" ->> 'displayField', + lk."JsonScalars" ->> 'displayColumn', + lk."JsonScalars" ->> 'textField', + lk."JsonScalars" ->> 'textColumn', + lk."JsonScalars" ->> 'nameField', + lk."JsonScalars" ->> 'nameColumn' + ), + lk."ParsedLookupDisplayColumn" + ), + + 'lookupJoinMode', CASE + WHEN lk."LookupJsonTextDecoded" IS NOT NULL AND fn."Fn" NOT LIKE '%id' + AND lower(COALESCE(lk."ParsedLookupValueColumn", '')) IN ('name', 'code', 'key', 'value') + THEN 'direct-string' + WHEN lk."LookupJsonTextDecoded" IS NOT NULL AND fn."Fn" NOT LIKE '%id' + THEN 'direct-string' + WHEN lk."LookupJsonTextDecoded" IS NOT NULL OR fn."Fn" LIKE '%id' + THEN 'convert-nvarchar-safe' + END, + + 'lookupFallbackToSourceColumn', ( + lk."LookupJsonTextDecoded" IS NOT NULL + AND fn."Fn" NOT LIKE '%id' + ), + + 'lookupSearchText', CASE + WHEN lk."LookupJsonTextDecoded" IS NOT NULL + THEN concat_ws(' ', f."FieldName", f."CaptionName", - lt_cap."Value"::TEXT, - lj.lookup_json_text_decoded + lt_cap."Value"::text, + lk."LookupJsonTextDecoded" ) END, - 'isActiveStatusColumn', - f."FieldName" IN - ( - 'IsActive', - 'IsEnabled', - 'IsPublished' + 'isActiveStatusColumn', fn."Fn" IN ('isactive', 'isenabled', 'ispublished'), + + 'isDeletedStatusColumn', fn."Fn" IN ('isdeleted', 'deleted', 'isremoved'), + + 'isSensitive', ( + fn."Fn" LIKE '%password%' + OR fn."Fn" LIKE '%connectionstring%' + OR fn."Fn" LIKE '%token%' + OR fn."Fn" LIKE '%secret%' + OR fn."Fn" LIKE '%apikey%' + OR fn."Fn" LIKE '%privatekey%' ), - 'isDeletedStatusColumn', - f."FieldName" IN - ( - 'IsDeleted', - 'Deleted', - 'IsRemoved' - ), - - 'isSensitive', - CASE - WHEN f."FieldName" LIKE '%Password%' - OR f."FieldName" LIKE '%ConnectionString%' - OR f."FieldName" LIKE '%Token%' - OR f."FieldName" LIKE '%Secret%' - OR f."FieldName" LIKE '%ApiKey%' - OR f."FieldName" LIKE '%PrivateKey%' - THEN TRUE - - ELSE FALSE - END, - - 'isDefaultSelectColumn', - CASE - WHEN - COALESCE( - f."Visible", - TRUE - ) = TRUE - - AND COALESCE( - f."IsActive", - TRUE - ) = TRUE - - AND f."FieldName" NOT LIKE '%Password%' - - AND f."FieldName" NOT LIKE '%ConnectionString%' - - AND f."FieldName" NOT LIKE '%Token%' - - AND f."FieldName" NOT LIKE '%Secret%' - - AND f."FieldName" NOT LIKE '%ApiKey%' - - AND f."FieldName" NOT LIKE '%PrivateKey%' - - AND f."FieldName" NOT IN - ( - 'Avatar', - 'ExtraProperties' - ) - - THEN TRUE - - ELSE FALSE - END - ) - - ORDER BY - COALESCE( - f."ListOrderNo", - 999999 - ), - f."FieldName" - ) - - FROM dbo."Sas_H_ListFormField" f - - /* - SQL Server: - sys.tables - sys.columns - TYPE_NAME() - - PostgreSQL: - information_schema.columns - */ - LEFT JOIN LATERAL - ( - SELECT - c.data_type AS sql_data_type - - FROM information_schema.columns c - + 'isDefaultSelectColumn', ( + COALESCE(f."Visible", TRUE) + AND COALESCE(f."IsActive", TRUE) + AND fn."Fn" NOT LIKE '%password%' + AND fn."Fn" NOT LIKE '%connectionstring%' + AND fn."Fn" NOT LIKE '%token%' + AND fn."Fn" NOT LIKE '%secret%' + AND fn."Fn" NOT LIKE '%apikey%' + AND fn."Fn" NOT LIKE '%privatekey%' + AND fn."Fn" NOT IN ('avatar', 'extraproperties') + ) + ) AS "FieldJson" + FROM public."Sas_H_ListFormField" f + -- SQL Server karsilastirmalari (LIKE/IN) buyuk/kucuk harf duyarsizdir. + CROSS JOIN LATERAL (SELECT lower(f."FieldName" COLLATE "default") AS "Fn") fn + LEFT JOIN "ListFormDictColumn" sc + ON sc."TableName" = lower(l."TableName" COLLATE "default") + AND sc."ColumnName" = fn."Fn" + LEFT JOIN "ListFormDictLookup" lk ON lk."FieldId" = f."Id" + LEFT JOIN "ListFormDictText" lt_cap ON lt_cap."Key" = lower(f."CaptionName" COLLATE "default") WHERE - c.table_schema = 'dbo' - - AND c.table_name = - l."TableName" - - AND c.column_name = - f."FieldName" - - LIMIT 1 - - ) sc ON TRUE - - - /* - LookupJson decode - */ - LEFT JOIN LATERAL - ( - SELECT - - NULLIF - ( - BTRIM - ( - REPLACE( - REPLACE( - REPLACE( - REPLACE( - REPLACE( - NULLIF( - BTRIM( - f."LookupJson"::TEXT - ), - '' - ), - - '\\u0022', - '"' - ), - - '\u0022', - '"' - ), - - '\\u0027', - '''' - ), - - '\u0027', - '''' - ), - - '"', - '"' - ) - ), - - '' - ) AS lookup_json_text_decoded - - ) lj_text ON TRUE - - - /* - PostgreSQL'de ISJSON yerine - güvenli JSON parse işlemi gerekiyor. - - LookupJson kolonu zaten JSON/JSONB ise - doğrudan kullanılabilir. - */ - LEFT JOIN LATERAL - ( - SELECT - - lj_text.lookup_json_text_decoded, - - CASE - - WHEN - lj_text.lookup_json_text_decoded - ~ '^\s*[\{\[]' - - THEN - lj_text.lookup_json_text_decoded::JSONB - - ELSE NULL - - END AS lookup_json - - ) lj ON TRUE - - - /* - LookupQuery değerini al - */ - LEFT JOIN LATERAL - ( - SELECT - - CASE - WHEN lj.lookup_json IS NOT NULL - THEN - lj.lookup_json - ->> 'LookupQuery' - END AS lookup_query_text - - ) lq ON TRUE - - - /* - LookupQuery temizleme - */ - LEFT JOIN LATERAL - ( - SELECT - - NULLIF - ( - BTRIM - ( - REGEXP_REPLACE - ( - REPLACE( - REPLACE( - REPLACE( - REPLACE( - COALESCE( - lq.lookup_query_text, - '' - ), - - '"', - '' - ), - - '[', - '' - ), - - ']', - '' - ), - - '`', - '' - ), - - '\s+', - ' ', - 'g' - ) - ), - - '' - ) AS clean_lookup_query - - ) lqc ON TRUE - - - /* - SQL Server prosedürü LookupQuery içerisindeki - SELECT ... FROM ... ifadesini string işlemleri - ile parse ediyor. - - PostgreSQL'de regexp_match daha temiz. - */ - LEFT JOIN LATERAL - ( - SELECT - - ( - REGEXP_MATCH - ( - lqc.clean_lookup_query, - - '(?i)\bFROM\s+([a-zA-Z0-9_\."]+)' - ) - )[1] AS parsed_table, - - COALESCE - ( - NULLIF( - lj.lookup_json - ->> 'ValueExpr', - '' - ), - - NULLIF( - lj.lookup_json - ->> 'valueExpr', - '' - ), - - NULLIF( - lj.lookup_json - ->> 'valueField', - '' - ), - - NULLIF( - lj.lookup_json - ->> 'valueColumn', - '' - ), - - NULLIF( - lj.lookup_json - ->> 'keyField', - '' - ), - - NULLIF( - lj.lookup_json - ->> 'keyFieldName', - '' - ) - ) - AS parsed_lookup_value_column, - - COALESCE - ( - NULLIF( - lj.lookup_json - ->> 'DisplayExpr', - '' - ), - - NULLIF( - lj.lookup_json - ->> 'displayExpr', - '' - ), - - NULLIF( - lj.lookup_json - ->> 'displayField', - '' - ), - - NULLIF( - lj.lookup_json - ->> 'displayColumn', - '' - ), - - NULLIF( - lj.lookup_json - ->> 'textField', - '' - ), - - NULLIF( - lj.lookup_json - ->> 'textColumn', - '' - ), - - NULLIF( - lj.lookup_json - ->> 'nameField', - '' - ), - - NULLIF( - lj.lookup_json - ->> 'nameColumn', - '' - ) - ) - AS parsed_lookup_display_column - - ) parsed ON TRUE - - - LEFT JOIN LATERAL - ( - SELECT - - NULLIF - ( - REGEXP_REPLACE - ( - COALESCE( - parsed.parsed_table, - '' - ), - - '^(?i:dbo\.)', - - '' - ), - - '' - ) - AS parsed_lookup_table_name, - - parsed.parsed_lookup_value_column, - - parsed.parsed_lookup_display_column - - ) lkp ON TRUE - - - /* - Caption translation - */ - LEFT JOIN LATERAL - ( - SELECT - lt."Value" - - FROM dbo."Sas_H_LanguageText" lt - - WHERE - lt."CultureName" = - p_ui_culture_name - - AND lt."Key" = - f."CaptionName" - + f."ListFormCode" = l."ListFormCode" + AND f."CultureName" = l."CultureName" + AND (p_DefinitionCultureName IS NULL OR f."CultureName" = p_DefinitionCultureName) AND ( - p_resource_name IS NULL - - OR lt."ResourceName" = - p_resource_name + p_OnlyBaseDefinitions = FALSE + OR (f."UserId" IS NULL AND f."RoleId" IS NULL) ) - - ORDER BY - lt."ResourceName" - - LIMIT 1 - - ) lt_cap ON TRUE - - - WHERE - - f."ListFormCode" = - l."ListFormCode" - - AND f."CultureName" = - l."CultureName" - - AND - ( - p_definition_culture_name IS NULL - - OR f."CultureName" = - p_definition_culture_name - ) - - AND - ( - p_only_base_definitions = FALSE - - OR - ( - f."UserId" IS NULL - AND f."RoleId" IS NULL - ) - ) - - ), - - '[]'::JSONB + ) fj + ) ) - ) - AS form_json, - - l."ListFormCode" - - FROM list_form_source l - ) - - SELECT - jsonb_build_object - ( - 'listForms', - - COALESCE - ( - jsonb_agg - ( - form_json - - ORDER BY - "ListFormCode" - ), - - '[]'::JSONB + ORDER BY l."ListFormCode" ) ) - - INTO v_result - - FROM list_forms_json; - - - RETURN COALESCE - ( - v_result, - - jsonb_build_object - ( - 'listForms', - '[]'::JSONB - ) - ); - + )::text END + FROM list_form_source l; END; -$$; \ No newline at end of file +$$; diff --git a/configs/seeds/postgres/object/Sas_H_OrderItem_ByTenantId.sql b/configs/seeds/postgres/object/Sas_H_OrderItem_ByTenantId.sql index 05d28723..40e8f737 100644 --- a/configs/seeds/postgres/object/Sas_H_OrderItem_ByTenantId.sql +++ b/configs/seeds/postgres/object/Sas_H_OrderItem_ByTenantId.sql @@ -1,6 +1,5 @@ -CREATE OR REPLACE VIEW dbo."Sas_H_OrderItem_ByTenantId" +CREATE OR REPLACE VIEW public."Sas_H_OrderItem_ByTenantId" AS - SELECT oi."Id", o."TenantId", @@ -15,8 +14,6 @@ SELECT oi."TotalPrice", oi."OrderId", o."IsActive" - -FROM dbo."Sas_H_Order" o - -INNER JOIN dbo."Sas_H_OrderItem" oi - ON o."Id" = oi."OrderId"; \ No newline at end of file +FROM public."Sas_H_Order" o +INNER JOIN public."Sas_H_OrderItem" oi + ON o."Id" = oi."OrderId"; diff --git a/configs/seeds/sql/object/Sas_H_ListFormFieldsDictionaryJson.sql b/configs/seeds/sql/object/Sas_H_ListFormFieldsDictionaryJson.sql index 2fb55143..be4c5957 100644 --- a/configs/seeds/sql/object/Sas_H_ListFormFieldsDictionaryJson.sql +++ b/configs/seeds/sql/object/Sas_H_ListFormFieldsDictionaryJson.sql @@ -11,6 +11,157 @@ AS BEGIN SET NOCOUNT ON; + -- Metin ve kolon tipi her ekran ve alan icin aranir. Dil metninin indeksi (ResourceName, Key) + -- Key ile baslamadigi icin dogrudan arama her alanda kulturun butun metinlerini tarar; ikisi + -- bir kez anahtarli tabloya alinir ve aramalar seek olur. + CREATE TABLE #Text + ( + [Key] NVARCHAR(128) COLLATE DATABASE_DEFAULT NOT NULL PRIMARY KEY, + [Value] NVARCHAR(MAX) NULL + ); + + INSERT INTO #Text ([Key], [Value]) + SELECT X.[Key], X.[Value] + FROM + ( + SELECT + LT.[Key], + LT.[Value], + RowNo = ROW_NUMBER() OVER (PARTITION BY LT.[Key] ORDER BY LT.ResourceName) + FROM dbo.Sas_H_LanguageText LT + WHERE LT.CultureName = @UiCultureName + AND LT.[Key] IS NOT NULL + AND (@ResourceName IS NULL OR LT.ResourceName = @ResourceName) + ) X + WHERE X.RowNo = 1; + + CREATE TABLE #Column + ( + TableName SYSNAME COLLATE DATABASE_DEFAULT NOT NULL, + ColumnName SYSNAME COLLATE DATABASE_DEFAULT NOT NULL, + SqlDataType SYSNAME NULL, + PRIMARY KEY (TableName, ColumnName) + ); + + INSERT INTO #Column (TableName, ColumnName, SqlDataType) + SELECT TBL.name, C.name, TYPE_NAME(C.user_type_id) + FROM sys.schemas S + INNER JOIN sys.tables TBL ON TBL.schema_id = S.schema_id + INNER JOIN sys.columns C ON C.object_id = TBL.object_id + WHERE S.name = N'dbo'; + + -- Lookup ayristirmasi alan basina bir kez yapilir. Ic ice APPLY zinciri sorguda ifade olarak + -- acilir; her basvuruda REPLACE/JSON_VALUE zinciri yeniden hesaplanir ve sozluk dakikalar surer. + CREATE TABLE #Lookup + ( + FieldId UNIQUEIDENTIFIER NOT NULL PRIMARY KEY, + LookupJsonTextDecoded NVARCHAR(MAX) NULL, + CleanLookupQuery NVARCHAR(MAX) NULL, + ParsedLookupTableName NVARCHAR(MAX) NULL, + ParsedLookupValueColumn NVARCHAR(MAX) NULL, + ParsedLookupDisplayColumn NVARCHAR(MAX) NULL + ); + + INSERT INTO #Lookup (FieldId, LookupJsonTextDecoded) + SELECT X.Id, X.LookupJsonTextDecoded + FROM + ( + SELECT + F.Id, + LookupJsonTextDecoded = NULLIF(LTRIM(RTRIM( + REPLACE( + REPLACE( + REPLACE( + REPLACE( + REPLACE( + NULLIF(LTRIM(RTRIM(CAST(F.LookupJson AS NVARCHAR(MAX)))), N''), + N'\\u0022', N'"'), + N'\u0022', N'"'), + N'\\u0027', N''''), + N'\u0027', N''''), + N'"', N'"') + )), N'') + FROM dbo.Sas_H_ListFormField F + ) X + WHERE X.LookupJsonTextDecoded IS NOT NULL; + + UPDATE LK + SET LK.CleanLookupQuery = NULLIF(LTRIM(RTRIM( + REPLACE( + REPLACE( + REPLACE( + REPLACE( + REPLACE( + REPLACE( + REPLACE( + REPLACE( + CASE WHEN ISJSON(LK.LookupJsonTextDecoded) = 1 THEN JSON_VALUE(LK.LookupJsonTextDecoded, '$.LookupQuery') END, + N'"', N''), + N'[', N''), + N']', N''), + N'`', N''), + NCHAR(13), N' '), + NCHAR(10), N' '), + NCHAR(9), N' '), + N' ', N' ') + )), N'') + FROM #Lookup LK; + + UPDATE LK + SET LK.ParsedLookupTableName = CASE + WHEN LQP.FromPos > 0 AND LQS.StopPos > LQP.FromPos + THEN NULLIF(REPLACE(REPLACE(LEFT( + LTRIM(RTRIM(SUBSTRING( + LK.CleanLookupQuery, + LQP.FromPos + 6, + LQS.StopPos - (LQP.FromPos + 6) + ))), + CHARINDEX(N' ', LTRIM(RTRIM(SUBSTRING( + LK.CleanLookupQuery, + LQP.FromPos + 6, + LQS.StopPos - (LQP.FromPos + 6) + ))) + N' ') - 1 + ), N'dbo.', N''), N'DBO.', N''), N'') + END, + LK.ParsedLookupValueColumn = COALESCE( + NULLIF(JSON_VALUE(LK.LookupJsonTextDecoded, '$.ValueExpr'), N''), + NULLIF(JSON_VALUE(LK.LookupJsonTextDecoded, '$.valueExpr'), N''), + NULLIF(JSON_VALUE(LK.LookupJsonTextDecoded, '$.valueField'), N''), + NULLIF(JSON_VALUE(LK.LookupJsonTextDecoded, '$.valueColumn'), N''), + NULLIF(JSON_VALUE(LK.LookupJsonTextDecoded, '$.keyField'), N''), + NULLIF(JSON_VALUE(LK.LookupJsonTextDecoded, '$.keyFieldName'), N'') + ), + LK.ParsedLookupDisplayColumn = COALESCE( + NULLIF(JSON_VALUE(LK.LookupJsonTextDecoded, '$.DisplayExpr'), N''), + NULLIF(JSON_VALUE(LK.LookupJsonTextDecoded, '$.displayExpr'), N''), + NULLIF(JSON_VALUE(LK.LookupJsonTextDecoded, '$.displayField'), N''), + NULLIF(JSON_VALUE(LK.LookupJsonTextDecoded, '$.displayColumn'), N''), + NULLIF(JSON_VALUE(LK.LookupJsonTextDecoded, '$.textField'), N''), + NULLIF(JSON_VALUE(LK.LookupJsonTextDecoded, '$.textColumn'), N''), + NULLIF(JSON_VALUE(LK.LookupJsonTextDecoded, '$.nameField'), N''), + NULLIF(JSON_VALUE(LK.LookupJsonTextDecoded, '$.nameColumn'), N'') + ) + FROM #Lookup LK + OUTER APPLY + ( + SELECT + FromPos = CHARINDEX(N' FROM ', UPPER(ISNULL(LK.CleanLookupQuery, N''))), + OrderPos = CHARINDEX(N' ORDER BY ', UPPER(ISNULL(LK.CleanLookupQuery, N''))), + WherePos = CHARINDEX(N' WHERE ', UPPER(ISNULL(LK.CleanLookupQuery, N''))), + GroupPos = CHARINDEX(N' GROUP BY ', UPPER(ISNULL(LK.CleanLookupQuery, N''))) + ) LQP + OUTER APPLY + ( + SELECT + StopPos = CASE + WHEN LQP.FromPos <= 0 THEN 0 + WHEN LQP.WherePos > LQP.FromPos THEN LQP.WherePos + WHEN LQP.GroupPos > LQP.FromPos THEN LQP.GroupPos + WHEN LQP.OrderPos > LQP.FromPos THEN LQP.OrderPos + ELSE LEN(LK.CleanLookupQuery) + 1 + END + ) LQS; + ;WITH ListFormSource AS ( SELECT @@ -29,33 +180,9 @@ BEGIN L.UserId, L.RoleId FROM dbo.Sas_H_ListForm L - OUTER APPLY - ( - SELECT TOP (1) LT.[Value] - FROM dbo.Sas_H_LanguageText LT - WHERE LT.CultureName = @UiCultureName - AND LT.[Key] = L.Name - AND (@ResourceName IS NULL OR LT.ResourceName = @ResourceName) - ORDER BY LT.ResourceName - ) LT_NAME - OUTER APPLY - ( - SELECT TOP (1) LT.[Value] - FROM dbo.Sas_H_LanguageText LT - WHERE LT.CultureName = @UiCultureName - AND LT.[Key] = L.Title - AND (@ResourceName IS NULL OR LT.ResourceName = @ResourceName) - ORDER BY LT.ResourceName - ) LT_TITLE - OUTER APPLY - ( - SELECT TOP (1) LT.[Value] - FROM dbo.Sas_H_LanguageText LT - WHERE LT.CultureName = @UiCultureName - AND LT.[Key] = L.Description - AND (@ResourceName IS NULL OR LT.ResourceName = @ResourceName) - ORDER BY LT.ResourceName - ) LT_DESC + LEFT JOIN #Text LT_NAME ON LT_NAME.[Key] = L.Name + LEFT JOIN #Text LT_TITLE ON LT_TITLE.[Key] = L.Title + LEFT JOIN #Text LT_DESC ON LT_DESC.[Key] = L.Description WHERE (@DefinitionCultureName IS NULL OR L.CultureName = @DefinitionCultureName) AND (@ListFormCode IS NULL OR L.ListFormCode = @ListFormCode) @@ -123,66 +250,66 @@ BEGIN ELSE 0 END) AS [isBooleanColumn], - LJ.LookupJsonTextDecoded AS [lookupJsonText], + LK.LookupJsonTextDecoded AS [lookupJsonText], COALESCE( - CASE WHEN ISJSON(LJ.LookupJsonTextDecoded) = 1 THEN COALESCE( - JSON_VALUE(LJ.LookupJsonTextDecoded, '$.tableName'), - JSON_VALUE(LJ.LookupJsonTextDecoded, '$.sqlTableName'), - JSON_VALUE(LJ.LookupJsonTextDecoded, '$.lookupTableName'), - JSON_VALUE(LJ.LookupJsonTextDecoded, '$.dataSource.tableName'), - JSON_VALUE(LJ.LookupJsonTextDecoded, '$.dataSource.sqlTableName') + CASE WHEN ISJSON(LK.LookupJsonTextDecoded) = 1 THEN COALESCE( + JSON_VALUE(LK.LookupJsonTextDecoded, '$.tableName'), + JSON_VALUE(LK.LookupJsonTextDecoded, '$.sqlTableName'), + JSON_VALUE(LK.LookupJsonTextDecoded, '$.lookupTableName'), + JSON_VALUE(LK.LookupJsonTextDecoded, '$.dataSource.tableName'), + JSON_VALUE(LK.LookupJsonTextDecoded, '$.dataSource.sqlTableName') ) END, - LKP.ParsedLookupTableName + LK.ParsedLookupTableName ) AS [lookupTableName], COALESCE( - CASE WHEN ISJSON(LJ.LookupJsonTextDecoded) = 1 THEN COALESCE( - JSON_VALUE(LJ.LookupJsonTextDecoded, '$.valueExpr'), - JSON_VALUE(LJ.LookupJsonTextDecoded, '$.valueField'), - JSON_VALUE(LJ.LookupJsonTextDecoded, '$.valueColumn'), - JSON_VALUE(LJ.LookupJsonTextDecoded, '$.keyField'), - JSON_VALUE(LJ.LookupJsonTextDecoded, '$.keyFieldName') + CASE WHEN ISJSON(LK.LookupJsonTextDecoded) = 1 THEN COALESCE( + JSON_VALUE(LK.LookupJsonTextDecoded, '$.valueExpr'), + JSON_VALUE(LK.LookupJsonTextDecoded, '$.valueField'), + JSON_VALUE(LK.LookupJsonTextDecoded, '$.valueColumn'), + JSON_VALUE(LK.LookupJsonTextDecoded, '$.keyField'), + JSON_VALUE(LK.LookupJsonTextDecoded, '$.keyFieldName') ) END, - LKP.ParsedLookupValueColumn + LK.ParsedLookupValueColumn ) AS [lookupValueColumn], COALESCE( - CASE WHEN ISJSON(LJ.LookupJsonTextDecoded) = 1 THEN COALESCE( - JSON_VALUE(LJ.LookupJsonTextDecoded, '$.displayExpr'), - JSON_VALUE(LJ.LookupJsonTextDecoded, '$.displayField'), - JSON_VALUE(LJ.LookupJsonTextDecoded, '$.displayColumn'), - JSON_VALUE(LJ.LookupJsonTextDecoded, '$.textField'), - JSON_VALUE(LJ.LookupJsonTextDecoded, '$.textColumn'), - JSON_VALUE(LJ.LookupJsonTextDecoded, '$.nameField'), - JSON_VALUE(LJ.LookupJsonTextDecoded, '$.nameColumn') + CASE WHEN ISJSON(LK.LookupJsonTextDecoded) = 1 THEN COALESCE( + JSON_VALUE(LK.LookupJsonTextDecoded, '$.displayExpr'), + JSON_VALUE(LK.LookupJsonTextDecoded, '$.displayField'), + JSON_VALUE(LK.LookupJsonTextDecoded, '$.displayColumn'), + JSON_VALUE(LK.LookupJsonTextDecoded, '$.textField'), + JSON_VALUE(LK.LookupJsonTextDecoded, '$.textColumn'), + JSON_VALUE(LK.LookupJsonTextDecoded, '$.nameField'), + JSON_VALUE(LK.LookupJsonTextDecoded, '$.nameColumn') ) END, - LKP.ParsedLookupDisplayColumn + LK.ParsedLookupDisplayColumn ) AS [lookupDisplayColumn], CASE - WHEN LJ.LookupJsonTextDecoded IS NOT NULL AND F.FieldName NOT LIKE N'%Id' - AND COALESCE(LKP.ParsedLookupValueColumn, N'') IN (N'Name', N'Code', N'Key', N'Value', N'key', N'value') + WHEN LK.LookupJsonTextDecoded IS NOT NULL AND F.FieldName NOT LIKE N'%Id' + AND COALESCE(LK.ParsedLookupValueColumn, N'') IN (N'Name', N'Code', N'Key', N'Value', N'key', N'value') THEN N'direct-string' - WHEN LJ.LookupJsonTextDecoded IS NOT NULL AND F.FieldName NOT LIKE N'%Id' + WHEN LK.LookupJsonTextDecoded IS NOT NULL AND F.FieldName NOT LIKE N'%Id' THEN N'direct-string' - WHEN LJ.LookupJsonTextDecoded IS NOT NULL OR F.FieldName LIKE N'%Id' + WHEN LK.LookupJsonTextDecoded IS NOT NULL OR F.FieldName LIKE N'%Id' THEN N'convert-nvarchar-safe' END AS [lookupJoinMode], CONVERT(bit, CASE - WHEN LJ.LookupJsonTextDecoded IS NOT NULL + WHEN LK.LookupJsonTextDecoded IS NOT NULL AND F.FieldName NOT LIKE N'%Id' THEN 1 ELSE 0 END) AS [lookupFallbackToSourceColumn], CASE - WHEN LJ.LookupJsonTextDecoded IS NOT NULL + WHEN LK.LookupJsonTextDecoded IS NOT NULL THEN CONCAT_WS(N' ', F.FieldName, F.CaptionName, CAST(LT_CAP.[Value] AS NVARCHAR(MAX)), - LJ.LookupJsonTextDecoded + LK.LookupJsonTextDecoded ) END AS [lookupSearchText], @@ -220,143 +347,11 @@ BEGIN END) AS [isDefaultSelectColumn] FROM dbo.Sas_H_ListFormField F - OUTER APPLY - ( - SELECT TOP (1) - SqlDataType = TYPE_NAME(C.user_type_id) - FROM sys.schemas S - INNER JOIN sys.tables TBL ON TBL.schema_id = S.schema_id - INNER JOIN sys.columns C ON C.object_id = TBL.object_id - WHERE S.name = N'dbo' - AND TBL.name = L.TableName - AND C.name = F.FieldName - ) SC - OUTER APPLY - ( - SELECT - LookupJsonTextRaw = NULLIF(LTRIM(RTRIM(CAST(F.LookupJson AS NVARCHAR(MAX)))), N'') - ) LJR - OUTER APPLY - ( - SELECT - LookupJsonTextDecoded = NULLIF(LTRIM(RTRIM( - REPLACE( - REPLACE( - REPLACE( - REPLACE( - REPLACE( - LJR.LookupJsonTextRaw, - N'\\u0022', N'"'), - N'\u0022', N'"'), - N'\\u0027', N''''), - N'\u0027', N''''), - N'"', N'"') - )), N'') - ) LJ - OUTER APPLY - ( - SELECT - LookupQueryText = CASE - WHEN ISJSON(LJ.LookupJsonTextDecoded) = 1 - THEN JSON_VALUE(LJ.LookupJsonTextDecoded, '$.LookupQuery') - END - ) LQ - OUTER APPLY - ( - SELECT - CleanLookupQuery = NULLIF(LTRIM(RTRIM( - REPLACE( - REPLACE( - REPLACE( - REPLACE( - REPLACE( - REPLACE( - REPLACE( - REPLACE( - LQ.LookupQueryText, - N'"', N''), - N'[', N''), - N']', N''), - N'`', N''), - NCHAR(13), N' '), - NCHAR(10), N' '), - NCHAR(9), N' '), - N' ', N' ') - )), N'') - ) LQC - OUTER APPLY - ( - SELECT - FromPos = CHARINDEX(N' FROM ', UPPER(ISNULL(LQC.CleanLookupQuery, N''))), - OrderPos = CHARINDEX(N' ORDER BY ', UPPER(ISNULL(LQC.CleanLookupQuery, N''))), - WherePos = CHARINDEX(N' WHERE ', UPPER(ISNULL(LQC.CleanLookupQuery, N''))), - GroupPos = CHARINDEX(N' GROUP BY ', UPPER(ISNULL(LQC.CleanLookupQuery, N''))), - SelectPart = CASE - WHEN CHARINDEX(N' FROM ', UPPER(ISNULL(LQC.CleanLookupQuery, N''))) > 0 - THEN LTRIM(RTRIM(SUBSTRING( - LQC.CleanLookupQuery, - 7, - CHARINDEX(N' FROM ', UPPER(LQC.CleanLookupQuery)) - 7 - ))) - END - ) LQP - OUTER APPLY - ( - SELECT - StopPos = CASE - WHEN LQP.FromPos <= 0 THEN 0 - WHEN LQP.WherePos > LQP.FromPos THEN LQP.WherePos - WHEN LQP.GroupPos > LQP.FromPos THEN LQP.GroupPos - WHEN LQP.OrderPos > LQP.FromPos THEN LQP.OrderPos - ELSE LEN(LQC.CleanLookupQuery) + 1 - END - ) LQS - OUTER APPLY - ( - SELECT - ParsedLookupTableName = CASE - WHEN LQP.FromPos > 0 AND LQS.StopPos > LQP.FromPos - THEN NULLIF(REPLACE(REPLACE(LEFT( - LTRIM(RTRIM(SUBSTRING( - LQC.CleanLookupQuery, - LQP.FromPos + 6, - LQS.StopPos - (LQP.FromPos + 6) - ))), - CHARINDEX(N' ', LTRIM(RTRIM(SUBSTRING( - LQC.CleanLookupQuery, - LQP.FromPos + 6, - LQS.StopPos - (LQP.FromPos + 6) - ))) + N' ') - 1 - ), N'dbo.', N''), N'DBO.', N''), N'') - END, - ParsedLookupValueColumn = COALESCE( - NULLIF(JSON_VALUE(LJ.LookupJsonTextDecoded, '$.ValueExpr'), N''), - NULLIF(JSON_VALUE(LJ.LookupJsonTextDecoded, '$.valueExpr'), N''), - NULLIF(JSON_VALUE(LJ.LookupJsonTextDecoded, '$.valueField'), N''), - NULLIF(JSON_VALUE(LJ.LookupJsonTextDecoded, '$.valueColumn'), N''), - NULLIF(JSON_VALUE(LJ.LookupJsonTextDecoded, '$.keyField'), N''), - NULLIF(JSON_VALUE(LJ.LookupJsonTextDecoded, '$.keyFieldName'), N'') - ), - ParsedLookupDisplayColumn = COALESCE( - NULLIF(JSON_VALUE(LJ.LookupJsonTextDecoded, '$.DisplayExpr'), N''), - NULLIF(JSON_VALUE(LJ.LookupJsonTextDecoded, '$.displayExpr'), N''), - NULLIF(JSON_VALUE(LJ.LookupJsonTextDecoded, '$.displayField'), N''), - NULLIF(JSON_VALUE(LJ.LookupJsonTextDecoded, '$.displayColumn'), N''), - NULLIF(JSON_VALUE(LJ.LookupJsonTextDecoded, '$.textField'), N''), - NULLIF(JSON_VALUE(LJ.LookupJsonTextDecoded, '$.textColumn'), N''), - NULLIF(JSON_VALUE(LJ.LookupJsonTextDecoded, '$.nameField'), N''), - NULLIF(JSON_VALUE(LJ.LookupJsonTextDecoded, '$.nameColumn'), N'') - ) - ) LKP - OUTER APPLY - ( - SELECT TOP (1) LT.[Value] - FROM dbo.Sas_H_LanguageText LT - WHERE LT.CultureName = @UiCultureName - AND LT.[Key] = F.CaptionName - AND (@ResourceName IS NULL OR LT.ResourceName = @ResourceName) - ORDER BY LT.ResourceName - ) LT_CAP + LEFT JOIN #Column SC + ON SC.TableName = L.TableName + AND SC.ColumnName = F.FieldName + LEFT JOIN #Lookup LK ON LK.FieldId = F.Id + LEFT JOIN #Text LT_CAP ON LT_CAP.[Key] = F.CaptionName WHERE F.ListFormCode = L.ListFormCode AND F.CultureName = L.CultureName diff --git a/configs/seeds/wizard/Contact.json b/configs/seeds/wizard/Contact.json index 3b7673f4..6aa87da5 100644 --- a/configs/seeds/wizard/Contact.json +++ b/configs/seeds/wizard/Contact.json @@ -3137,11 +3137,11 @@ "DataSourceType": 2, "ValueExpr": "Key", "DisplayExpr": "Name", - "CascadeRelationField": null, + "CascadeRelationField": "", "CascadeFilterOperator": "=", - "CascadeParentFields": null, - "CascadeEmptyFields": null, - "LookupQuery": "SELECT Name AS [Key], Name AS [Name] FROM Adm_T_ContactTag WHERE IsDeleted = 'false' AND IsActive = 1 ORDER BY Name" + "CascadeParentFields": "", + "CascadeEmptyFields": "", + "LookupQuery": "SELECT Name AS [Key], Name AS [Name], Color AS [Color] FROM Adm_T_ContactTag WHERE IsDeleted = 'false' AND IsActive = 1 ORDER BY Name" }, "ValidationRuleDto": [], "ColumnStylingDto": [], diff --git a/ui/src/views/form/editors/TagBoxEditorComponent.tsx b/ui/src/views/form/editors/TagBoxEditorComponent.tsx index 8003f7bd..6c9bc7a5 100644 --- a/ui/src/views/form/editors/TagBoxEditorComponent.tsx +++ b/ui/src/views/form/editors/TagBoxEditorComponent.tsx @@ -1,6 +1,7 @@ import { ColumnFormatDto, TagBoxOptionsDto } from '@/proxy/form/models' import TagBox from 'devextreme-react/tag-box' import { ReactElement, useMemo } from 'react' +import { createColoredTagRender } from '../../shared/lookupColor' const TagBoxEditorComponent = ({ value, @@ -21,6 +22,10 @@ const TagBoxEditorComponent = ({ // Her render'da yeni dizi verilirse TagBox bunu değer değişikliği sayar ve onValueChanged döngüye girer. const val = useMemo(() => (Array.isArray(value) ? value : [value]), [value]) const lookupDto = col?.lookupDto + const tagRender = useMemo( + () => createColoredTagRender(lookupDto?.displayExpr?.toLocaleLowerCase()), + [lookupDto?.displayExpr], + ) // const dataSource = // typeof lookup?.dataSource === 'function' @@ -34,6 +39,7 @@ const TagBoxEditorComponent = ({ dataSource={editorOptions?.dataSource} valueExpr={lookupDto?.valueExpr?.toLocaleLowerCase()} displayExpr={lookupDto?.displayExpr?.toLocaleLowerCase()} + tagRender={tagRender} showClearButton={options?.showClearButton} showSelectionControls={options?.showSelectionControls ?? true} maxDisplayedTags={options?.maxDisplayedTags} diff --git a/ui/src/views/list/GridColumnData.ts b/ui/src/views/list/GridColumnData.ts index 7ec72f5e..59fb0f34 100644 --- a/ui/src/views/list/GridColumnData.ts +++ b/ui/src/views/list/GridColumnData.ts @@ -14,6 +14,8 @@ interface IGridColumnData extends DataGridTypes.Column { tagBoxOptions?: TagBoxOptionsDto gridBoxOptions?: GridBoxOptionsDto imageUploadOptions?: ImageUploadOptionsDto + /** Lookup değeri → `Color`; çok değerli rozetler bu renkle çizilir. */ + lookupColors?: Map } caption?: string isBand?: boolean diff --git a/ui/src/views/list/editors/TagBoxEditorComponent.tsx b/ui/src/views/list/editors/TagBoxEditorComponent.tsx index 5c2556fd..42c26f5c 100644 --- a/ui/src/views/list/editors/TagBoxEditorComponent.tsx +++ b/ui/src/views/list/editors/TagBoxEditorComponent.tsx @@ -1,5 +1,6 @@ import TagBox from 'devextreme-react/tag-box' import { ReactElement } from 'react' +import { createColoredTagRender } from '../../shared/lookupColor' const TagBoxEditorComponent = (cellElement: any): ReactElement => { const col = cellElement.column @@ -22,6 +23,7 @@ const TagBoxEditorComponent = (cellElement: any): ReactElement => { dataSource={dataSource} valueExpr={lookup?.valueExpr} displayExpr={lookup?.displayExpr} + tagRender={createColoredTagRender(lookup?.displayExpr)} showClearButton={options?.showClearButton} showSelectionControls={options?.showSelectionControls ?? true} maxDisplayedTags={options?.maxDisplayedTags} diff --git a/ui/src/views/list/useListFormColumns.ts b/ui/src/views/list/useListFormColumns.ts index aa3f0450..3ce12ff5 100644 --- a/ui/src/views/list/useListFormColumns.ts +++ b/ui/src/views/list/useListFormColumns.ts @@ -32,32 +32,35 @@ import { thumbnailSource, } from '../form/editors/imageHoverPreview' import { translateWorkflowLabel } from '@/utils/workflow/workflowHelpers' +import { findLookupColor, getLookupItemColor, tagBadgeColors } from '../shared/lookupColor' const cellTemplateMultiValue = ( cellElement: HTMLElement, cellInfo: DataGridTypes.ColumnCellTemplateData, ) => { if (cellInfo?.value) { - const values = Array.isArray(cellInfo.value) - ? cellInfo.value.map((a: any) => { - const { lookup } = cellInfo.column - if (lookup && lookup.calculateCellValue) { - return lookup.calculateCellValue(a) - } - return '' - }) - : [cellInfo.value] + const { lookup } = cellInfo.column + const colors: Map | undefined = (cellInfo.column as GridColumnData).extras + ?.lookupColors + const colorOf = (value: any) => colors?.get(String(value)) ?? findLookupColor(lookup, value) + const badges = Array.isArray(cellInfo.value) + ? cellInfo.value.map((a: any) => ({ + text: lookup?.calculateCellValue ? lookup.calculateCellValue(a) : '', + ...tagBadgeColors(colorOf(a)), + })) + : [{ text: cellInfo.value, ...tagBadgeColors(colorOf(cellInfo.value)) }] + const values = badges.map((b) => b.text) // Badge benzeri HTML üret - const html = values - .filter((v) => v) + const html = badges + .filter((b) => b.text) .map( - (v) => ` + (b) => `
- ${v} + ${b.text}
`, ) @@ -78,6 +81,46 @@ const cellTemplateMultiValue = ( } } +/** + * Rozet renkleri lookup'ın kendi isteğinden (aynı önbellek girdisi) okunur; DevExtreme'in + * yüklediği öğeler kolon nesnesinde güvenilir biçimde taşınmaz. Harita, grid'in lookup + * yüklemesiyle aynı promise'ten dolduğu için hücreler çizilmeden önce hazırdır. + */ +const loadLookupColors = (source: any, valueExpr?: string) => { + const colors = new Map() + const store = + source instanceof CustomStore ? source : (source as { store?: CustomStore } | undefined)?.store + Promise.resolve(store?.load?.()) + .then((items: any) => { + const expr = valueExpr?.toLowerCase() + ;((items ?? []) as Record[]).forEach((item) => { + const color = getLookupItemColor(item) + const key = Object.keys(item).find((k) => k.toLowerCase() === expr) + if (color && key) colors.set(String(item[key]), color) + }) + }) + .catch(() => undefined) + return colors +} + +const cellTemplateColor = ( + cellElement: HTMLElement, + cellInfo: DataGridTypes.ColumnCellTemplateData, +) => { + const value = cellInfo?.value + if (typeof value !== 'string' || !value.trim()) return + + cellElement.style.cssText += 'display:flex;align-items:center;gap:8px;' + const swatch = document.createElement('span') + // Geçersiz renk değeri style'a hiç yazılmaz; kutu boş görünür, metin yine gösterilir. + swatch.style.backgroundColor = value + swatch.style.cssText += + 'display:inline-block;width:16px;height:16px;border-radius:4px;border:1px solid rgba(128,128,128,.5);flex-shrink:0;' + const text = document.createElement('span') + text.textContent = cellInfo.text || value + cellElement.replaceChildren(swatch, text) +} + // Görsel hücre şablonu. Normalizasyon ve hover önizleme ortak modüllerden gelir. const cellTemplateImage = ( cellElement: HTMLElement, @@ -629,6 +672,10 @@ const useListFormColumns = ({ column.encodeHtml = false } + if (colData.editorType2 === 'dxColorBox' && !colData.lookupDto?.dataSourceType) { + column.cellTemplate = cellTemplateColor + } + // #region lookup ayarlari if (colData.lookupDto?.dataSourceType) { // UiColumnEditorTemplateTypeEnum : None:0, Table:1, TagBox:2 @@ -660,6 +707,13 @@ const useListFormColumns = ({ } } + if (column.extras && !colData.lookupDto.cascadeParentFields) { + column.extras.lookupColors = loadLookupColors( + lookupDataSource({}, colData), + colData.lookupDto.valueExpr, + ) + } + column.lookup = { valueExpr: colData.lookupDto?.valueExpr?.toLowerCase(), displayExpr: colData.lookupDto?.displayExpr?.toLowerCase(), diff --git a/ui/src/views/shared/lookupColor.tsx b/ui/src/views/shared/lookupColor.tsx new file mode 100644 index 00000000..c8645fa4 --- /dev/null +++ b/ui/src/views/shared/lookupColor.tsx @@ -0,0 +1,60 @@ +import { ReactElement } from 'react' + +const DEFAULT_TAG_COLOR = '#dc3545' + +/** Lookup öğesinin rengi; sorgu `Color` kolonu döndürüyorsa (`StaticData`'da `color`) kullanılır. */ +export const getLookupItemColor = (item: any): string | undefined => { + if (!item || typeof item !== 'object') return undefined + const color = item.Color ?? item.color + return typeof color === 'string' && color.trim() ? color.trim() : undefined +} + +/** Grid lookup'ının yüklenmiş öğelerinden (`lookup.items`) değere karşılık gelen rengi bulur. */ +export const findLookupColor = (lookup: any, value: any): string | undefined => { + const items: any[] | undefined = lookup?.items + if (!items?.length || !lookup.valueExpr) return undefined + const item = items.find((i) => i?.[lookup.valueExpr] == value) + return getLookupItemColor(item) +} + +/** Açık zeminde beyaz yazı okunmadığı için yazı rengi zeminin parlaklığına göre seçilir. */ +export const readableTextColor = (color: string): string => { + const hex = color.replace('#', '') + const full = + hex.length === 3 + ? hex + .split('') + .map((c) => c + c) + .join('') + : hex.slice(0, 6) + if (!/^[0-9a-f]{6}$/i.test(full)) return '#fff' + const r = parseInt(full.slice(0, 2), 16) + const g = parseInt(full.slice(2, 4), 16) + const b = parseInt(full.slice(4, 6), 16) + return (r * 299 + g * 587 + b * 114) / 1000 > 150 ? '#111827' : '#fff' +} + +export const tagBadgeColors = (color?: string) => { + const background = color ?? DEFAULT_TAG_COLOR + return { background, textColor: readableTextColor(background) } +} + +/** + * Renk taşıyan lookup için TagBox etiketi. DevExtreme'in varsayılan etiket yapısı korunur; + * kaldırma düğmesi `dx-tag-remove-button` sınıfıyla çalıştığı için aynen yazılır. + */ +export const createColoredTagRender = (displayExpr?: string) => + function ColoredTag(item: any): ReactElement { + const text = + item && typeof item === 'object' ? (displayExpr ? item[displayExpr] : item.name) : item + const color = getLookupItemColor(item) + return ( +
+ {text} +
+
+ ) + }