İzin Yönetimi

This commit is contained in:
Sedat ÖZTÜRK 2026-10-04 22:17:16 +03:00
parent 7ecb45a53f
commit 98eeea8dae
60 changed files with 22891 additions and 295 deletions

View file

@ -1374,7 +1374,7 @@ listesinde `Hr` varsa görür (modül listesi boş kiracıda süzme yoktur — `
| **Definitions › Department** | `App.Hr.Department` | `Hr_T_Department` | tenant | **Şube bazlı** departman ağacı: ilk sütun `BranchId` (Şube Adı; `LookupQueryValues.TenantBranchValues`, zorunlu), `ParentId` lookup'ı seçili şubenin departmanlarına süzülür (`BranchDepartmentValues`, cascade `BranchId`). `ParentId` ile **ağaç (TreeList) düzeninin** referansı; personel/onay ekranlarının departman lookup'ı. Tekil indeks `TenantId + BranchId + Name`. Seed `data/App.Hr.Department.json`. |
| **Definitions › Job Position** | `App.Hr.JobPosition` | `Hr_T_JobPosition` | tenant | Ünvan ağacı. Form sırası `BranchId` → `DepartmentId` (şubeye göre süzülür) → `Name` → `ParentId` (departmana göre süzülür); şube değişince departman ve üst görev temizlenir (`CascadeEmptyFields`). **İki kademeli cascade** örneği. Seed `data/App.Hr.JobPosition.json`. |
| **Work Calendar** | `App.Hr.WorkCalendar` | `Hr_T_WorkCalendar` | tenant | Resmi tatil ve çalışılmayan günler: `Name`, `StartDate`, `EndDate`, `DayType` (`Holiday`/`HalfDay`), `RecurrenceRule` (boş = tek sefer, `FREQ=YEARLY` = her yıl), `AllDay` (gizli, `true`), `IsActive`. Grid'e ek olarak **Takvim (Scheduler)** görünümü açıktır (`TextExpr = Name`). İzin ve puantaj hesabının tatil kaynağıdır. Wizard `wizard/WorkCalendar.json`, tablo `sql/object/Hr_T_WorkCalendar.sql`, seed `data/App.Hr.WorkCalendar.json` (sabit tarihli resmi tatiller her yıl tekrar eder; Ramazan ve Kurban bayramları hicri takvime göre kaydığı için 2026–2027 tarihleriyle tek seferlik girilir, sonraki yıllar ekrandan eklenir). |
| **Definitions › Leave Type** | `App.Hr.LeaveType` | `Hr_T_LeaveType` | tenant | İzin türü: `Code`, `Name`, `Gender` (`All`/`Male`/`Female`), `MaxDaysPerYear`, `IsPaid`, `DeductsFromAnnualLeave`, `RequiresDocument`, `AllowHalfDay`, `IsActive`. İzin talebi/bakiye ekranlarının izin türü lookup'ı. Wizard `wizard/LeaveType.json`, tablo `sql/object/Hr_T_LeaveType.sql`, seed `data/App.Hr.LeaveType.json` (Türkiye standart izin türleri). |
| **Definitions › Leave Type** | `App.Hr.LeaveType` | `Hr_T_LeaveType` | tenant | İzin türü: `Code`, `Name`, `Gender` (`All`/`Male`/`Female`), `MaxDaysPerYear`, `IsPaid`, `DeductsFromAnnualLeave`, `RequiresDocument`, `AllowHalfDay`, `BalanceExceedAction` (`Block`/`Warn`), `IsActive`. İzin Yönetimi'nin tür kuralları; `Sick` kodlu tür Sağlık Kartı istirahatiyle eşlenir. Wizard `wizard/LeaveType.json`, tablo `sql/object/Hr_T_LeaveType.sql`, seed `data/App.Hr.LeaveType.json` (Türkiye standart izin türleri). |
| **Definitions › Expense Type** | `App.Hr.ExpenseType` | `Hr_T_ExpenseType` | tenant | Masraf türü: `Code`, `Name`, `DefaultAmount`, `CurrencyId` (`Sas_H_Currency` lookup'ı), `IsTaxable`, `IsSgkSubject`, `RequiresReceipt`, `IsActive`. Masraf beyanı ekranlarının lookup'ı. Wizard `wizard/ExpenseType.json`, tablo `sql/object/Hr_T_ExpenseType.sql`, seed `data/App.Hr.ExpenseType.json`. |
| **Definitions › Benefit Type** | `App.Hr.BenefitType` | `Hr_T_BenefitType` | tenant | Yan hak türü: `Code`, `Name`, `DefaultAmount`, `CurrencyId`, `IsTaxable`, `IsSgkSubject`, `IsActive`. Kodu kullanıcının yan hak kaydında (`Adm_T_UserBenefit.BenefitType`, Identity › Users › Benefits) değer olarak saklanır; kod değiştirilirse eski kayıtlar eşleşmez. Wizard `wizard/BenefitType.json`, tablo `sql/object/Hr_T_BenefitType.sql`, seed `data/App.Hr.BenefitType.json`. |
| **Shift** | `App.Hr.Shift` | `Hr_T_Shift` | tenant | Vardiya: `Name`, `StartTime`/`EndTime` (`HH:mm`), `BreakMinutes`, `WorkingHours`, `IsNightShift`, `IsActive`. Seed `data/App.Hr.Shift.json`. |
@ -1396,6 +1396,7 @@ listesinde `Hr` varsa görür (modül listesi boş kiracıda süzme yoktur — `
| **Recruitment › Interview** | `App.Hr.Interview` (alt form `App.Hr.InterviewScore`) | `Hr_T_Interview` · `Hr_T_InterviewScore` | tenant | Mülakat planlama: tür, başlangıç/bitiş (**Scheduler** görünümü), mülakatçı, durum, öneri. Kayıt eklenince mülakatçıya `IseAlimMulakatAtandi` bildirimi SQL ile yazılır (tipin `Custom` kuralları kanalı belirler, metin `App.Hr.Interview.Notification.*`). Puan alt formu kriter başına 1–5; `OverallScore` = Σ(puan × ağırlık) / Σ ağırlık, başvurunun `AverageScore`'u mülakatların ortalaması. Wizard. |
| **Recruitment › Job Offer** | `App.Hr.JobOffer` | `Hr_T_JobOffer` | tenant | Teklif: `OfferNo` (`TKL-…`), başvuru seçilince pozisyon bilgileri ilandan `copy` ile dolar, brüt ücret + para birimi, yan haklar (`Hr_T_BenefitType` kodları), `Status` (`Draft`/`Sent`/`Accepted`/`Declined`/`Expired`, yanıt tarihi otomatik). İç onay **Grid Workflow** (Kadro Talebi ile aynı akış). Onaylı satırda **Teklif Mektubu** butonu (`DynamicReport`, `Raporlar` kategorisi). Wizard. |
| **Performance Management** | `App.Hr.Performance` dalı (yalnız İK): `PerformancePeriod`, `PerformanceReview`, `PerformanceCalibration`, `DevelopmentPlan`; tanım `App.Hr.PerformanceRating` (İK › Tanımlar). Çalışana ait `MyPerformanceReview`, `TeamPerformanceReview`, `My360Feedback` menüsüzdür; Intranet › Dashboard'daki custom widget'lardan (`MyPerformanceWidget`, `TeamPerformanceWidget`, `My360FeedbackWidget`) açılır | `Hr_T_PerformanceRating` · `Hr_T_PerformancePeriod` · `Hr_T_PerformancePeriodSkill` · `Hr_T_PerformanceReview` · `Hr_T_PerformanceGoal` · `Hr_T_PerformanceCompetency` · `Hr_T_PerformanceFeedback` · `Hr_T_PerformanceFeedbackScore` · `Hr_T_DevelopmentPlan` | tenant | Dönem → değerlendirme (hedef KPI/OKR + yetkinlik `Hr_T_Skill`/`Hr_T_SkillLevel` + isteğe bağlı 360) → kalibrasyon → gelişim planı. **Aynı tabloya persona ekranları**: İK (tümü), çalışan (`EmployeeId = '@USERID'`), yönetici (`ManagerId = '@USERID'`), 360 değerlendiricisi (`ReviewerId = '@USERID'`); kilitli alanlar İK ekranının yetkisine bağlıdır (sunucu düşürür), satır sahipliği ve aşama kuralı `Hr_S_PerformanceGuard` (before-command). Hesap ve durum geçişi `Hr_S_PerformanceReviewRecalc` / `Hr_S_PerformanceReviewAfterSave`; tamamlanan değerlendirme `Adm_T_UserSkill`'e yazar. Widget'lar veriyi ekranın kendi sorgusundan (`list-form-select/select`) okur. Çalışma şekli ve kararlar: `configs/seeds/host/docs/modules/HrPerformance.md`. Wizard + Custom Component (pano kutusu). |
| **Leave Management** | `App.Hr.Leave` dalı (yalnız İK): `LeaveRequest` (Grid Workflow: Müdür Onayı `@ManagerUserNames` → İK Onayı; üst pozisyonu olmayan çalışanda müdür adımı `RequiresManagerApproval` karşılaştırmasıyla atlanır), `LeaveBalance` (alt formlar `LeaveBalanceAdjustment`, `LeaveBalanceYear`). Çalışana ait `MyLeaveRequest`, `MyLeaveBalance` ve müdüre ait `TeamLeaveRequest` menüsüzdür; Intranet › Dashboard'daki `MyLeaveWidget` / `TeamLeaveWidget`'tan açılır; kullanıcı detayında **İzinler** sekmesi | `Hr_T_LeaveRequest` · `Hr_T_LeaveBalanceAdjustment` · görünümler `Hr_V_LeaveBalanceYear` · `Hr_V_LeaveSummary` | tenant | Gün sayısı `Hr_F_LeaveWorkDays` (Ayarlar › İnsan Kaynakları › İzin çalışma günleri + `Hr_T_WorkCalendar`); bakiye İş Kanunu md.53 kademeleri (ayarlardan) + devir + düzeltme; müdür = departmanın en üst pozisyonunun sahibi. Doğrulama, müdür çözümü, rapor ↔ `Adm_T_UserSickLeave` eşlemesi ve bildirim `Hr_S_LeaveRequestAfterSave`; satır koruması `Hr_S_LeaveRequestGuard`; Sağlık Kartı tarafı `Hr_S_LeaveSickLeaveSync`. SQL'den ayar okuma `Hr_F_HrSetting` (kiracı → genel → varsayılan). Çalışma şekli: `configs/seeds/host/docs/modules/HrLeave.md`. Wizard + Custom Component + küçük UI/motor eki. |
| **Org Chart** | `App.Hr.OrgChart` · `/admin/organization` | `Hr_T_Department` · `Hr_T_JobPosition` | tenant | Departman/ünvan verisinin organizasyon şeması görünümü (özel React sayfası `hr/OrgChart`, veri `api/app/org-chart`). Menü ve route aynı yetkiyi ister. |
| **Intranet › Announcement** | `App.Hr.Intranet.Announcement` (alt formlar Like/Comment) | `Hr_T_Announcement` · `Hr_T_AnnouncementLike` · `Hr_T_AnnouncementComment` | tenant | Duyuru + beğeni + yorum: **ana kayıt / alt etkileşim** kalıbının hazır örneği. |
| **Intranet › Survey** | `App.Hr.Intranet.Survey` (alt form Question → Option; Response → Answer) | `Hr_T_Survey` · `Hr_T_SurveyQuestion` · `Hr_T_SurveyQuestionOption` · `Hr_T_SurveyResponse` · `Hr_T_SurveyAnswer` | tenant | Anket motoru; soru–seçenek–yanıt zinciri **iki seviyeli alt form** ihtiyacının referansıdır. |
@ -1514,6 +1515,7 @@ alanları da doldurur (`lowcode` §0.9).
| "eğitim / katılımcı, araç / şoför / araç masrafı / araç çıkış-giriş, olay günlüğü, disiplin / tutanak / savunma / ihtar" | İK › Trainings · İK › Vehicles · İK › Event Log · İK › Disciplinary Case | Var olan `Hr_T_Training*` / `Hr_T_Vehicle*` / `Hr_T_Driver*` / `Hr_T_EventLog` / `Hr_T_Disciplinary*` ekranını genişlet ya da lookup bağla; paralel tablo üretme |
| "işe alım, kadro talebi, ilan, aday / CV, mülakat, teklif" | İK › Recruitment | Var olan `Hr_T_StaffRequest` / `Hr_T_JobPosting` / `Hr_T_Candidate` / `Hr_T_JobApplication` / `Hr_T_Interview` / `Hr_T_JobOffer` ekranını genişlet; personel kullanıcısını BT elle açar, aday kaydına `UserId` ile bağlanır |
| "performans değerlendirme, hedef / KPI / OKR, yetkinlik değerlendirmesi, 360, kalibrasyon, gelişim planı" | İK › Performance Management | Var olan `Hr_T_Performance*` / `Hr_T_DevelopmentPlan` ekranını genişlet; yeni persona ekranı aynı tabloya `DefaultFilter` + `Hr_S_PerformanceGuard` ile açılır (`configs/seeds/host/docs/modules/HrPerformance.md`) |
| "izin talebi, izin onayı, izin bakiyesi, yıllık izin hakkı, rapor/istirahat izni" | İK › Leave Management | Var olan `Hr_T_LeaveRequest` ekranını genişlet; persona ekranı aynı tabloya `DefaultFilter` + `Hr_S_LeaveRequestGuard` ile açılır, bakiye `Hr_V_LeaveSummary`'den okunur (`configs/seeds/host/docs/modules/HrLeave.md`) |
| "yapay zekâ cevaplasın" | Saas › Definitions › AiBot | Bot tanımı; `/admin/ai` bu tanımı kullanır |
### 12.7 Canlı referans uygulamalar

View file

@ -1422,7 +1422,7 @@ cascade eksik yetenek değildir ve analizde `Ready to Start`'ı engellemez. Tam
| `DataSourceType` | `1` StaticData · `2` Query · `3` WebService |
| `ValueExpr` / `DisplayExpr` | Varsayılan `Key` / `Name` |
| `LookupQuery` | Kaynağın kendisi (tipe göre biçim değişir, aşağıda) |
| `CascadeParentFields` | **Çocuk alanda** doldurulur; değerleri sorguya taşınacak ebeveyn alan(lar), virgülle. Sırayla `@param0`, `@param1`… olur (`Query`/`WebService`); boş ya da gelmeyen ebeveyn `NULL` bağlanır. Değer her zaman **metin** parametre olarak bağlanır (`ListFormSelectAppService.ToLookupParameter`), sayı anahtarlı ebeveyn de çalışır; dönüşümü kolon tipine göre veritabanı yapar. `WebService` gövdesine değer JSON kaçışlı yazılır |
| `CascadeParentFields` | **Çocuk alanda** doldurulur; değerleri sorguya taşınacak ebeveyn alan(lar), virgülle. Sırayla `@param0`, `@param1`… olur (`Query`/`WebService`); boş ya da gelmeyen ebeveyn `NULL` bağlanır. Ebeveyn çocuğu, değeri boşken kilitler (bool ebeveynde `false` da boştur) ve değiştiğinde temizler; ebeveyn yalnızca gerçekten süzülen liste için yazılır, hesap için kullanılmaz. Değer her zaman **metin** parametre olarak bağlanır (`ListFormSelectAppService.ToLookupParameter`), sayı anahtarlı ebeveyn de çalışır; dönüşümü kolon tipine göre veritabanı yapar. `WebService` gövdesine değer JSON kaçışlı yazılır |
| `CascadeRelationField` | **Çocuk alanda**, yalnızca `StaticData`'da anlamlıdır: öğede ilk ebeveynin değeriyle karşılaştırılan alan; boşsa `group`. Ebeveyn boşken liste süzülmez |
| `CascadeFilterOperator` | `StaticData` cascade filtresinin operatörü; varsayılan `=` |
| `CascadeEmptyFields` | **Ebeveyn alanda** doldurulur; bu alan değişince boşaltılacak çocuk alanlar, virgülle. Ara halka (İl) hem çocuk hem ebeveyn olduğu için iki tarafı birden taşır |
@ -2122,8 +2122,18 @@ Kurallar:
geçer. Bu yüzden dosyada `N1`, `N2` yazman yeterlidir ve **tekil olmaları şarttır**.
- `Compare` düğümünde çok dallı karar için `CompareOutcomes` kullanılır; iki dallı basit karar
için `NextOnTrue` / `NextOnFalse` yeterlidir.
Karşılaştırma **sayısaldır** (`EvaluateCondition`: `>`, `>=`, `<`, `<=`, `=`, `!=`); bit/metin kolon
okunamaz ve hata verir. Kayda göre adım atlamak için prosedür sayısal bir bayrak kolonu yazar ve
kolon ekranda gizli alan olur (örnek: İzin Yönetimi'nde üst pozisyonu olmayan çalışanın müdür
adımı, `RequiresManagerApproval`, `HrLeave.md` §5.6).
- `Approval` düğümü `NextOnApprove` / `NextOnReject`, `Start` ve `Inform` düğümleri
`NextOnStart` alanını kullanır. `End` düğümünün çıkışı yoktur.
- **Kayda göre değişen onaycı:** `Approver` değeri `@Kolon` ise (ör. `@ManagerUserNames`) onaycı kaydın o
kolonundan okunur (`ListFormWorkflowAppService.ResolveNodeApprover`); kolon `|` ile ayrılmış kullanıcı
adı/e-posta taşır ve ekranın alanı olmalıdır (satır ListForm sorgusundan okunur; gizli alan yeter). Kolon
boşsa akış hata verir — boş onaycı herkese onay yetkisi verirdi. Kolonu kayıt sonrası prosedür doldurur
(örnek: İzin Yönetimi'nde departman müdürü, `HrLeave.md`). Tasarımcıda onaycı listesi kullanıcılarla
birlikte `Alan: {Kolon}` seçeneklerini sunar.
- **`Inform` düğümü e-posta gönderir** (MailQueue üzerinden, `Approver` alanındaki kullanıcı/rol
adreslerine, önceki adımların notlarıyla birlikte). Uygulama içi bildirim ya da SMS isteniyorsa
ayrıca bir bildirim kuralı tanımlanır (§9.11).
@ -2561,9 +2571,9 @@ Script `AsyncFunction` ile derlenir, aşağıdaki adlar parametre olarak enjekte
| `pick(nesne, 'a.b')` | Herhangi bir nesneden yol okuma |
| `set('Alan', deger)` · `set({A:1, B:2})` | Yazar. Değişmeyen değer yazılmaz; yazmalar biriktirilip **tek flush** ile forma ve düzenlenen grid hücresine uygulanır |
| `clear('A','B')` | Verilen alanları `null` yapar |
| `copy('kolon','Alan')` | Lookup/GridBox'ta **seçili kayıttan** taşır; hedef verilmezse yolun son parçası hedef olur |
| `copy('kolon','Alan')` | Lookup/GridBox'ta **seçili kayıttan** taşır; hedef verilmezse yolun son parçası hedef olur. Seçili kayıt, olay formdan gelse de (popup form, form sayfası) değişen alanın editöründen okunur; lookup sorgusu taşınacak kolonları döndürmelidir (ör. `SELECT Id AS [Key], …, DepartmentId, JobPositionId`) |
| `selected('Yol')` · `snum()` · `sstr()` | Seçili kaydın tamamı / sayı / metin hâli |
| `readOnly('Alan', kosul)` | Formdaki **herhangi bir** editörü kilitler/açar (yalnızca script'in bağlı olduğu alanı değil) |
| `readOnly('Alan', kosul)` | Formdaki **herhangi bir** editörü kilitler/açar (yalnızca script'in bağlı olduğu alanı değil). Grid popup'ında açılış çalışması (`isReady`) editörler her yeniden çizildiğinde tekrarlanır ve form verisi o anda güncel değildir; koşul başka bir alana bakıyorsa değer `editor.component.cellValue(satır, 'Alan')` ile okunur (örnek: `HrLeave.md` §5.1) |
| `round(x, 2)` · `sum('A','B')` | Matematik |
| `days('Bas','Bit')` · `hours('Bas','Bit')` | Gün farkı (bitiş dahil) · saat farkı (negatifse +24) |
| `tpl('{Alan} - {value}')` | Şablon: `{Alan}`, `{value}`, `{selected.Yol}` |
@ -2571,7 +2581,7 @@ Script `AsyncFunction` ile derlenir, aşağıdaki adlar parametre olarak enjekte
| `ask('mesaj')` | `window.confirm`; hayır denirse `revert()` çalışır ve `false` döner |
| `revert()` | Alanı değişiklik öncesi değerine döndürür (editöre de yazar) |
| `openUrl('/adres', '_blank')` | Yeni sekme/pencere |
| `await api(url, 'data.name', options?)` | Ham `fetch`. **Kimlik doğrulama başlığı eklemez**; korumalı bir uç için Custom Endpoint tarafında erişim açık olmalıdır |
| `await api(url, 'data.name', options?)` | `/api/` ile başlayan uygulama adresi platform istemcisinden gider (oturum + kiracı başlığı; `options.method`/`options.body` taşınır). Dış adres ham `fetch`'tir, başlık eklemez. |
| `await geoTo('Lat', 'Lng', 'Acc'?)` | Cihaz konumunu alanlara yazar. İzin yoksa/okunamazsa alanlara **dokunmaz** ve uyarı gösterir, `false` döner |
| `$` | Tüm yardımcıların tek nesne hâli |

View file

@ -947,6 +947,8 @@ kullanıcı (personel) kaydını BT elle açar; İK aday kaydını `UserId` ile
`lowcode-reference.instructions.md` §12.4. **Performans Yönetimi** (`App.Hr.Performance`) wizard +
custom component ile üretilmiştir: İK menüsü şartları ve takibi taşır, çalışana ait listeler menüsüzdür ve
Intranet panosundaki widget'lardan açılır; çalışma şekli [HrPerformance.md](configs/seeds/host/docs/modules/HrPerformance.md).
**İzin Yönetimi** (`App.Hr.Leave`) aynı kalıptadır (talep, Müdür → İK Grid Workflow onayı, İş Kanunu md.53 bakiyesi,
rapor ↔ Sağlık Kartı eşlemesi, kullanıcı detayında İzinler sekmesi); çalışma şekli [HrLeave.md](configs/seeds/host/docs/modules/HrLeave.md).
**Yetki kodları** `PlatformConsts.AppCodes` içinde merkezî olarak tanımlıdır ve
`configs/seeds/host/data/Abp.Identity.Permissions.json` ile seed edilir (gruplar:
@ -1213,6 +1215,7 @@ başlığındaki ⓘ simgesi ise ekranın kendi ayrıntılı açıklamasını g
| --- | --- | --- |
| Performans Yönetimi | İK › Performans Yönetimi (`App.Hr.Performance`) | [HrPerformance.md](configs/seeds/host/docs/modules/HrPerformance.md) |
| İşe Alım | İK › İşe Alım (`App.Hr.Recruitment`) | [HrRecruitment.md](configs/seeds/host/docs/modules/HrRecruitment.md) |
| İzin Yönetimi | İK › İzin Yönetimi (`App.Hr.Leave`) | [HrLeave.md](configs/seeds/host/docs/modules/HrLeave.md) |
| Eğitimler | İK › Eğitimler (`App.Hr.Trainings`) | [HrTrainings.md](configs/seeds/host/docs/modules/HrTrainings.md) |
| Araçlar | İK › Araçlar (`App.Hr.Vehicles`) | [HrVehicles.md](configs/seeds/host/docs/modules/HrVehicles.md) |
| Disiplin Dosyaları | İK › Disiplin Dosyaları (`App.Hr.DisciplinaryCase`) | [HrDisciplinaryCase.md](configs/seeds/host/docs/modules/HrDisciplinaryCase.md) |
@ -1303,6 +1306,10 @@ Bildirim **tip + kanal** eşleşmesiyle yönlendirilir. Alıcı hedefleme seçen
`All`, `User`, `Role`, `OrganizationUnit`, `Custom`. Kullanıcılar kendi bildirim tercihlerini
`/admin/profile/notification-settings` üzerinden kanal bazında yönetir.
Bildirim panelinde, masaüstü bildiriminde ve Activity Log'da başlık kaydın **`Subject`**'idir
(`NotificationDto.Subject`; bildirimi yazan prosedür dil anahtarından çevrilmiş metni yazar). Tip adı
(`PerformansDurumDegisti` gibi) koddur; yalnız `Subject` boşsa gösterilir.
### 13.3. Kayıt durumu: gönderildi → okundu
Bir bildirim kaydı iki bayrak taşır ve sıra değişmez: **önce `IsSent`, sonra `IsRead`**.

View file

@ -7,6 +7,9 @@ public class NotificationDto : EntityDto<Guid>
{
public required string NotificationChannel { get; set; }
public required string NotificationType { get; set; }
/// <summary>Bildirimi üreten yerin yazdığı, çevrilmiş başlık; tip adı (ör. PerformansDurumDegisti) kod olduğu için başlık olarak gösterilmez.</summary>
public string? Subject { get; set; }
public required string Message { get; set; }
public bool IsRead { get; set; }
public bool IsSent { get; set; }

View file

@ -30,6 +30,13 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
private const int CriteriaIdPadding = 3;
private const string SystemApprovalDescription = "Sistem tarafından otomatik olarak onaylandı.";
/// <summary>
/// Düğümün onaycısı bu önekle yazılırsa (<c>@ManagerUserNames</c>) kayıttaki o kolonun değeri
/// onaycıdır: onaycı kayda göre değiştiğinde (ör. çalışanın departman müdürü) düğüme sabit
/// kullanıcı yazılamaz.
/// </summary>
private const string RowApproverPrefix = "@";
private readonly IRepository<ListFormWorkflow, string> criteriaRepository;
private readonly IRepository<Note, Guid> noteRepository;
private readonly IListFormManager listFormManager;
@ -578,7 +585,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
{
if (!context.Workflow.ApprovalUserFieldName.IsNullOrWhiteSpace())
{
update[context.Workflow.ApprovalUserFieldName] = node.Approver ?? string.Empty;
update[context.Workflow.ApprovalUserFieldName] = ResolveNodeApprover(context, node);
}
}
else if (node.Kind == "End")
@ -614,7 +621,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
private async Task SendInformEmailAsync(WorkflowRunContext context, ListFormWorkflow node)
{
var recipientEmails = await ResolveApproverEmailsAsync(node.Approver);
var recipientEmails = await ResolveApproverEmailsAsync(ResolveNodeApprover(context, node));
var senderName = await settingProvider.GetOrNullAsync(SeedConsts.AbpSettings.Mailing.Default.DefaultFromDisplayName);
var senderEmail = await settingProvider.GetOrNullAsync(SeedConsts.AbpSettings.Mailing.Default.DefaultFromAddress);
KeyValuePair<string, string>? sender = null;
@ -690,7 +697,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
var approverValue = GetRowValue(context.Row, context.Workflow.ApprovalUserFieldName)?.ToString();
if (approverValue.IsNullOrWhiteSpace())
{
approverValue = current.Approver;
approverValue = ResolveNodeApprover(context, current);
}
var approvers = SplitWorkflowApprovers(approverValue)
@ -759,7 +766,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
var keyText = string.Join(", ", context.Keys.Select(key => WebUtility.HtmlEncode(key?.ToString() ?? string.Empty)));
var listFormCode = WebUtility.HtmlEncode(context.ListFormCode ?? string.Empty);
var nodeTitle = WebUtility.HtmlEncode(node.Title ?? string.Empty);
var recipient = WebUtility.HtmlEncode(FormatWorkflowApprovers(node.Approver));
var recipient = WebUtility.HtmlEncode(FormatWorkflowApprovers(DisplayNodeApprover(context, node)));
var processRows = BuildWorkflowNoteContent(context.WorkflowNoteRows);
var previousNotesSection = previousWorkflowNotesHtml.IsNullOrWhiteSpace()
? string.Empty
@ -828,7 +835,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
var approverLabel = node.Kind == "Inform"
? NoteLabel("App.WorkflowNote.Inform", "Inform")
: NoteLabel("App.WorkflowNote.Approver", "Approver");
context.WorkflowNoteRows.Add((approverLabel, FormatWorkflowApprovers(node.Approver)));
context.WorkflowNoteRows.Add((approverLabel, FormatWorkflowApprovers(DisplayNodeApprover(context, node))));
}
}
@ -845,13 +852,13 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
localizer["App.ListFormWorkflow.WorkflowStarted"].Value,
localizer["App.ListFormWorkflow.Step", node.Title].Value,
localizer["App.ListFormWorkflow.PerformedBy", userName].Value
}.Concat(FormatNextToastNode(next)),
}.Concat(FormatNextToastNode(context, next)),
"Inform" => new[]
{
localizer["App.ListFormWorkflow.InformReached"].Value,
localizer["App.ListFormWorkflow.Step", node.Title].Value,
localizer["App.ListFormWorkflow.InformUser", FormatToastUser(node.Approver)].Value
}.Concat(FormatNextToastNode(next)),
localizer["App.ListFormWorkflow.InformUser", FormatToastUser(DisplayNodeApprover(context, node))].Value
}.Concat(FormatNextToastNode(context, next)),
"End" => new[]
{
localizer["App.ListFormWorkflow.WorkflowCompleted"].Value,
@ -887,7 +894,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
return null;
}
private IEnumerable<string> FormatNextToastNode(ListFormWorkflow? node)
private IEnumerable<string> FormatNextToastNode(WorkflowRunContext context, ListFormWorkflow? node)
{
if (node == null)
{
@ -900,13 +907,13 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
[
localizer["App.WorkflowNextStep.Approval"].Value,
localizer["App.ListFormWorkflow.NextStepName", node.Title].Value,
localizer["App.ListFormWorkflow.ApproverUser", FormatToastUser(node.Approver)].Value
localizer["App.ListFormWorkflow.ApproverUser", FormatToastUser(DisplayNodeApprover(context, node))].Value
],
"Inform" =>
[
localizer["App.WorkflowNextStep.Inform"].Value,
localizer["App.ListFormWorkflow.NextStepName", node.Title].Value,
localizer["App.ListFormWorkflow.InformUser", FormatToastUser(node.Approver)].Value
localizer["App.ListFormWorkflow.InformUser", FormatToastUser(DisplayNodeApprover(context, node))].Value
],
"End" =>
[
@ -924,6 +931,41 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
: FormatWorkflowApprovers(userName);
}
/// <summary>
/// Onay ve bilgilendirme için düğümün onaycısı. <see cref="RowApproverPrefix"/> ile yazılmış
/// onaycı kayıttan okunur; boş kalırsa akış durur — boş onaycı herkese onay yetkisi verirdi.
/// </summary>
private static string ResolveNodeApprover(WorkflowRunContext context, ListFormWorkflow node)
{
var approver = node.Approver?.Trim() ?? string.Empty;
if (!approver.StartsWith(RowApproverPrefix, StringComparison.Ordinal))
{
return approver;
}
var fieldName = approver[RowApproverPrefix.Length..];
var value = GetRowValue(context.Row, fieldName)?.ToString();
if (value.IsNullOrWhiteSpace())
{
throw new UserFriendlyException($"Workflow onaycısı kayıttaki '{fieldName}' alanından okunamadı; alan boş.");
}
return value;
}
/// <summary>Not ve bildirim metinleri için onaycı; kayıttan okunamazsa alan adı gösterilir.</summary>
private static string DisplayNodeApprover(WorkflowRunContext context, ListFormWorkflow node)
{
var approver = node.Approver?.Trim() ?? string.Empty;
if (!approver.StartsWith(RowApproverPrefix, StringComparison.Ordinal))
{
return approver;
}
var value = GetRowValue(context.Row, approver[RowApproverPrefix.Length..])?.ToString();
return value.IsNullOrWhiteSpace() ? approver : value;
}
private static string FormatWorkflowApprovers(string value)
{
return string.Join(", ", SplitWorkflowApprovers(value));

View file

@ -4458,6 +4458,14 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
PagerOptionJson = DefaultPagerOptionJson,
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
// Istirahat ile Izin Yonetimi'ndeki rapor talebi tek kayittir (Hr_S_LeaveSickLeaveSync).
UpdateFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
new() { FieldName = "LastModificationTime", FieldDbType = DbType.DateTime, Value = "@NOW", CustomValueType = FieldCustomValueTypeEnum.CustomKey },
new() { FieldName = "LastModifierId", FieldDbType = DbType.Guid, Value = "@USERID", CustomValueType = FieldCustomValueTypeEnum.CustomKey }
}),
InsertAfterCommand = "EXEC dbo.Hr_S_LeaveSickLeaveSync @Id, @CreatorId",
UpdateAfterCommand = "DECLARE @_id uniqueidentifier = (SELECT TOP 1 Id FROM Adm_T_UserSickLeave WHERE Id IN @Id); EXEC dbo.Hr_S_LeaveSickLeaveSync @_id, @LastModifierId",
DeleteAfterCommand = "UPDATE Hr_T_LeaveRequest SET IsDeleted = 1, DeletionTime = GETUTCDATE(), DeleterId = @DeleterId WHERE SickLeaveId IN @Id AND IsDeleted = 0",
EditingOptionJson = DefaultEditingOptionJson(listFormName, 700, 450, true, true, true, true, false),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>
{

View file

@ -11203,6 +11203,526 @@
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.Leave",
"ParentName": null,
"DisplayName": "App.Hr.Leave",
"Order": 24,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.LeaveRequest",
"ParentName": "App.Hr.Leave",
"DisplayName": "App.Hr.LeaveRequest",
"Order": 1,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.LeaveRequest.Create",
"ParentName": "App.Hr.LeaveRequest",
"DisplayName": "App.Platform.Create",
"Order": 1,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.LeaveRequest.Update",
"ParentName": "App.Hr.LeaveRequest",
"DisplayName": "App.Platform.Update",
"Order": 2,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.LeaveRequest.Delete",
"ParentName": "App.Hr.LeaveRequest",
"DisplayName": "App.Platform.Delete",
"Order": 3,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.LeaveRequest.Export",
"ParentName": "App.Hr.LeaveRequest",
"DisplayName": "App.Platform.Export",
"Order": 4,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.LeaveRequest.Import",
"ParentName": "App.Hr.LeaveRequest",
"DisplayName": "App.Platform.Import",
"Order": 5,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.LeaveRequest.Note",
"ParentName": "App.Hr.LeaveRequest",
"DisplayName": "App.Platform.Note",
"Order": 6,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.LeaveBalance",
"ParentName": "App.Hr.Leave",
"DisplayName": "App.Hr.LeaveBalance",
"Order": 2,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.LeaveBalance.Create",
"ParentName": "App.Hr.LeaveBalance",
"DisplayName": "App.Platform.Create",
"Order": 1,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.LeaveBalance.Update",
"ParentName": "App.Hr.LeaveBalance",
"DisplayName": "App.Platform.Update",
"Order": 2,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.LeaveBalance.Delete",
"ParentName": "App.Hr.LeaveBalance",
"DisplayName": "App.Platform.Delete",
"Order": 3,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.LeaveBalance.Export",
"ParentName": "App.Hr.LeaveBalance",
"DisplayName": "App.Platform.Export",
"Order": 4,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.LeaveBalance.Import",
"ParentName": "App.Hr.LeaveBalance",
"DisplayName": "App.Platform.Import",
"Order": 5,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.LeaveBalance.Note",
"ParentName": "App.Hr.LeaveBalance",
"DisplayName": "App.Platform.Note",
"Order": 6,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.LeaveBalanceAdjustment",
"ParentName": "App.Hr.LeaveBalance",
"DisplayName": "App.Hr.LeaveBalanceAdjustment",
"Order": 7,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.LeaveBalanceAdjustment.Create",
"ParentName": "App.Hr.LeaveBalanceAdjustment",
"DisplayName": "App.Platform.Create",
"Order": 1,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.LeaveBalanceAdjustment.Update",
"ParentName": "App.Hr.LeaveBalanceAdjustment",
"DisplayName": "App.Platform.Update",
"Order": 2,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.LeaveBalanceAdjustment.Delete",
"ParentName": "App.Hr.LeaveBalanceAdjustment",
"DisplayName": "App.Platform.Delete",
"Order": 3,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.LeaveBalanceAdjustment.Export",
"ParentName": "App.Hr.LeaveBalanceAdjustment",
"DisplayName": "App.Platform.Export",
"Order": 4,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.LeaveBalanceAdjustment.Import",
"ParentName": "App.Hr.LeaveBalanceAdjustment",
"DisplayName": "App.Platform.Import",
"Order": 5,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.LeaveBalanceAdjustment.Note",
"ParentName": "App.Hr.LeaveBalanceAdjustment",
"DisplayName": "App.Platform.Note",
"Order": 6,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.LeaveBalanceYear",
"ParentName": "App.Hr.LeaveBalance",
"DisplayName": "App.Hr.LeaveBalanceYear",
"Order": 8,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.LeaveBalanceYear.Create",
"ParentName": "App.Hr.LeaveBalanceYear",
"DisplayName": "App.Platform.Create",
"Order": 1,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.LeaveBalanceYear.Update",
"ParentName": "App.Hr.LeaveBalanceYear",
"DisplayName": "App.Platform.Update",
"Order": 2,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.LeaveBalanceYear.Delete",
"ParentName": "App.Hr.LeaveBalanceYear",
"DisplayName": "App.Platform.Delete",
"Order": 3,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.LeaveBalanceYear.Export",
"ParentName": "App.Hr.LeaveBalanceYear",
"DisplayName": "App.Platform.Export",
"Order": 4,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.LeaveBalanceYear.Import",
"ParentName": "App.Hr.LeaveBalanceYear",
"DisplayName": "App.Platform.Import",
"Order": 5,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.LeaveBalanceYear.Note",
"ParentName": "App.Hr.LeaveBalanceYear",
"DisplayName": "App.Platform.Note",
"Order": 6,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.MyLeaveRequest",
"ParentName": null,
"DisplayName": "App.Hr.MyLeaveRequest",
"Order": 25,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.MyLeaveRequest.Create",
"ParentName": "App.Hr.MyLeaveRequest",
"DisplayName": "App.Platform.Create",
"Order": 1,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.MyLeaveRequest.Update",
"ParentName": "App.Hr.MyLeaveRequest",
"DisplayName": "App.Platform.Update",
"Order": 2,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.MyLeaveRequest.Delete",
"ParentName": "App.Hr.MyLeaveRequest",
"DisplayName": "App.Platform.Delete",
"Order": 3,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.MyLeaveRequest.Export",
"ParentName": "App.Hr.MyLeaveRequest",
"DisplayName": "App.Platform.Export",
"Order": 4,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.MyLeaveRequest.Import",
"ParentName": "App.Hr.MyLeaveRequest",
"DisplayName": "App.Platform.Import",
"Order": 5,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.MyLeaveRequest.Note",
"ParentName": "App.Hr.MyLeaveRequest",
"DisplayName": "App.Platform.Note",
"Order": 6,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.MyLeaveBalance",
"ParentName": null,
"DisplayName": "App.Hr.MyLeaveBalance",
"Order": 26,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.MyLeaveBalance.Create",
"ParentName": "App.Hr.MyLeaveBalance",
"DisplayName": "App.Platform.Create",
"Order": 1,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.MyLeaveBalance.Update",
"ParentName": "App.Hr.MyLeaveBalance",
"DisplayName": "App.Platform.Update",
"Order": 2,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.MyLeaveBalance.Delete",
"ParentName": "App.Hr.MyLeaveBalance",
"DisplayName": "App.Platform.Delete",
"Order": 3,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.MyLeaveBalance.Export",
"ParentName": "App.Hr.MyLeaveBalance",
"DisplayName": "App.Platform.Export",
"Order": 4,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.MyLeaveBalance.Import",
"ParentName": "App.Hr.MyLeaveBalance",
"DisplayName": "App.Platform.Import",
"Order": 5,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.MyLeaveBalance.Note",
"ParentName": "App.Hr.MyLeaveBalance",
"DisplayName": "App.Platform.Note",
"Order": 6,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.TeamLeaveRequest",
"ParentName": null,
"DisplayName": "App.Hr.TeamLeaveRequest",
"Order": 27,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.TeamLeaveRequest.Create",
"ParentName": "App.Hr.TeamLeaveRequest",
"DisplayName": "App.Platform.Create",
"Order": 1,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.TeamLeaveRequest.Update",
"ParentName": "App.Hr.TeamLeaveRequest",
"DisplayName": "App.Platform.Update",
"Order": 2,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.TeamLeaveRequest.Delete",
"ParentName": "App.Hr.TeamLeaveRequest",
"DisplayName": "App.Platform.Delete",
"Order": 3,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.TeamLeaveRequest.Export",
"ParentName": "App.Hr.TeamLeaveRequest",
"DisplayName": "App.Platform.Export",
"Order": 4,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.TeamLeaveRequest.Import",
"ParentName": "App.Hr.TeamLeaveRequest",
"DisplayName": "App.Platform.Import",
"Order": 5,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Hr",
"Name": "App.Hr.TeamLeaveRequest.Note",
"ParentName": "App.Hr.TeamLeaveRequest",
"DisplayName": "App.Platform.Note",
"Order": 6,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Administration",
"Name": "App.HrSettings",
"ParentName": "App.Setting",
"DisplayName": "App.HrSettings",
"Order": 6,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
},
{
"GroupName": "App.Administration",
"Name": "App.HrSettings.Leave",
"ParentName": "App.HrSettings",
"DisplayName": "App.HrSettings.Leave",
"Order": 1,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Sas|Hr"
}
]
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,29 @@
{
"ListFormCode": "App.DeveloperKit.CustomComponents",
"KeyFieldName": "Id",
"GeneratedAt": "2026-10-04T15:00:00Z",
"Order": 0,
"Rows": [
{
"Id": "3b7b16c6-aa96-5765-8e19-fe5e727feb52",
"Name": "TeamLeaveWidget",
"RoutePath": null,
"Description": "Intranet panosu: departman müdürünün onayını bekleyen izin talepleri ve bugün izinde olanlar",
"IsActive": true,
"Dependencies": [],
"Code": "// Departman muduru icin ekibin izin ozeti. Satirlar ekranin kendi sorgusundan gelir: liste\n// yalniz muduru oldugu departmanin taleplerini dondurur (ManagerIds suzmesi ListForm'da).\nconst REQUESTS = 'App.Hr.TeamLeaveRequest'\nconst MANAGER_STEP = 'Müdür Onayı'\nconst APPROVED = 'Onaylandı'\n\nconst loadRows = async (listFormCode) => {\n const response = await apiService.fetchData({\n method: 'GET',\n url: '/api/app/list-form-select/select',\n params: { listFormCode, take: 500 },\n })\n return response.data?.data ?? []\n}\n\nconst formatDays = (value) => {\n const number = Number(value ?? 0)\n return Number.isInteger(number) ? String(number) : number.toFixed(1)\n}\n\nconst formatDate = (value) => {\n if (!value) return '-'\n const [year, month, day] = String(value).slice(0, 10).split('-')\n return `${day}.${month}.${year}`\n}\n\nconst Stat = ({ label, value, className }) => (\n <div className=\"rounded-lg border border-gray-200 bg-gray-50 p-2 text-center dark:border-gray-700 dark:bg-gray-900/40\">\n <div className={`text-lg font-semibold ${className}`}>{value}</div>\n <div className=\"truncate text-xs text-gray-500 dark:text-gray-400\">{label}</div>\n </div>\n)\n\nconst TeamLeaveWidget = () => {\n const [rows, setRows] = useState([])\n const [loading, setLoading] = useState(true)\n\n useEffect(() => {\n let active = true\n loadRows(REQUESTS)\n .then((result) => active && setRows(result))\n .catch(() => {})\n .finally(() => active && setLoading(false))\n return () => {\n active = false\n }\n }, [])\n\n const today = new Date().toISOString().slice(0, 10)\n const awaiting = rows\n .filter((row) => row.ApprovalStatus === MANAGER_STEP)\n .sort((a, b) => String(a.StartDate).localeCompare(String(b.StartDate)))\n const onLeave = rows.filter(\n (row) =>\n row.ApprovalStatus === APPROVED &&\n String(row.StartDate).slice(0, 10) <= today &&\n String(row.EndDate).slice(0, 10) >= today,\n )\n\n return (\n <div className=\"overflow-hidden rounded-xl border border-gray-200/50 bg-white shadow-lg dark:border-gray-700/50 dark:bg-gray-800\">\n <div className=\"flex items-center gap-2 border-b border-gray-200 p-4 dark:border-gray-700\">\n <PlatformIcon name=\"FaUserCheck\" className=\"h-5 w-5 text-indigo-500\" />\n <h2 className=\"text-base font-semibold text-gray-900 dark:text-white\">\n {translate('::App.DashboardWidget.TeamLeave')}\n </h2>\n </div>\n <div className=\"space-y-3 p-3\">\n {loading ? (\n <div className=\"p-4 text-center text-sm text-gray-500\">...</div>\n ) : (\n <>\n <div className=\"grid grid-cols-2 gap-2\">\n <Stat\n label={translate('::App.DashboardWidget.Leave.AwaitingMe')}\n value={awaiting.length}\n className=\"text-orange-500\"\n />\n <Stat\n label={translate('::App.DashboardWidget.Leave.OnLeaveToday')}\n value={onLeave.length}\n className=\"text-indigo-600 dark:text-indigo-400\"\n />\n </div>\n {awaiting.length === 0 ? (\n <div className=\"text-sm text-gray-500 dark:text-gray-400\">\n {translate('::App.DashboardWidget.Leave.NoPending')}\n </div>\n ) : (\n <ul className=\"space-y-1\">\n {awaiting.slice(0, 5).map((row) => (\n <li\n key={row.Id}\n className=\"flex items-center justify-between gap-2 rounded-md bg-gray-50 px-2 py-1 text-xs dark:bg-gray-900/40\"\n >\n <span className=\"truncate font-medium text-gray-800 dark:text-gray-100\">{row.EmployeeName}</span>\n <span className=\"shrink-0 text-gray-600 dark:text-gray-300\">\n {formatDate(row.StartDate)} · {formatDays(row.Days)} {translate('::App.DashboardWidget.Leave.Days')}\n </span>\n </li>\n ))}\n </ul>\n )}\n </>\n )}\n <div className=\"pt-1\">\n <a\n href={'/admin/list/' + REQUESTS}\n className=\"text-sm font-medium text-indigo-600 hover:underline dark:text-indigo-400\"\n >\n {translate('::App.DashboardWidget.Leave.OpenApprovals')} →\n </a>\n </div>\n </div>\n </div>\n )\n}\n\nexport default TeamLeaveWidget\n",
"Props": null,
"DataSources": [],
"SeoTitle": null,
"SeoDescription": null,
"SeoKeywords": null,
"SeoOgImage": null,
"SeoCanonicalUrl": null,
"SeoNoIndex": true,
"SeoIncludeInSitemap": false,
"SeoChangeFrequency": null,
"SeoPriority": null,
"SeoStructuredData": null
}
]
}

View file

@ -119,6 +119,32 @@
"Order": 90,
"Props": "",
"IsActive": true
},
{
"Id": "d031eff6-1e6e-5925-aa57-32b4f6da6136",
"Code": "my-leave",
"Name": "My Leaves",
"LabelKey": "App.DashboardWidget.MyLeave",
"SourceType": 1,
"ComponentName": "MyLeaveWidget",
"PermissionName": "App.Hr.MyLeaveRequest",
"Column": 0,
"Order": 35,
"Props": "",
"IsActive": true
},
{
"Id": "69e495dc-f46e-5a77-b674-8a9d1b18c850",
"Code": "team-leave",
"Name": "Team Leaves",
"LabelKey": "App.DashboardWidget.TeamLeave",
"SourceType": 1,
"ComponentName": "TeamLeaveWidget",
"PermissionName": "App.Hr.TeamLeaveRequest",
"Column": 0,
"Order": 36,
"Props": "",
"IsActive": true
}
]
}

View file

@ -13,6 +13,7 @@
"DeductsFromAnnualLeave": true,
"RequiresDocument": false,
"AllowHalfDay": true,
"BalanceExceedAction": "Block",
"IsActive": true,
"Id": "08615087-48ee-5cc2-b32b-1178fff77593"
},
@ -25,6 +26,7 @@
"DeductsFromAnnualLeave": false,
"RequiresDocument": false,
"AllowHalfDay": true,
"BalanceExceedAction": "Warn",
"IsActive": true,
"Id": "986f9678-1b46-5197-844d-16bf772a343a"
},
@ -37,6 +39,7 @@
"DeductsFromAnnualLeave": false,
"RequiresDocument": false,
"AllowHalfDay": true,
"BalanceExceedAction": "Warn",
"IsActive": true,
"Id": "030adbff-8bff-51ac-8cc0-07496a616c91"
},
@ -49,6 +52,7 @@
"DeductsFromAnnualLeave": false,
"RequiresDocument": true,
"AllowHalfDay": false,
"BalanceExceedAction": "Warn",
"IsActive": true,
"Id": "3b82ab6e-d846-583d-afc6-59cc97ba4a21"
},
@ -61,6 +65,7 @@
"DeductsFromAnnualLeave": false,
"RequiresDocument": true,
"AllowHalfDay": false,
"BalanceExceedAction": "Block",
"IsActive": true,
"Id": "7e391128-01b3-58a3-911a-fc300960ae2b"
},
@ -73,6 +78,7 @@
"DeductsFromAnnualLeave": false,
"RequiresDocument": true,
"AllowHalfDay": false,
"BalanceExceedAction": "Block",
"IsActive": true,
"Id": "2caac0cf-91b9-5693-bb30-dcfca42e33c9"
},
@ -85,6 +91,7 @@
"DeductsFromAnnualLeave": false,
"RequiresDocument": true,
"AllowHalfDay": false,
"BalanceExceedAction": "Block",
"IsActive": true,
"Id": "b744305a-d519-5b6a-8bfe-f7dae3d7c76f"
},
@ -97,6 +104,7 @@
"DeductsFromAnnualLeave": false,
"RequiresDocument": true,
"AllowHalfDay": false,
"BalanceExceedAction": "Block",
"IsActive": true,
"Id": "5f61e945-848b-57ab-a266-9b3ef76b58b5"
},
@ -109,6 +117,7 @@
"DeductsFromAnnualLeave": false,
"RequiresDocument": false,
"AllowHalfDay": true,
"BalanceExceedAction": "Warn",
"IsActive": true,
"Id": "77df8125-8af7-5d2e-89b3-084e23d5a4e8"
}

File diff suppressed because it is too large Load diff

View file

@ -2163,6 +2163,51 @@
"IsDisabled": false,
"ElementId": null,
"ModuleId": "Hr"
},
{
"Id": "c3ac8beb-302f-5285-89a5-9c98d2f19524",
"Code": "App.Hr.Leave",
"DisplayName": "App.Hr.Leave",
"Order": 14,
"Url": null,
"Icon": "FcOvertime",
"ParentCode": "App.Hr",
"RequiredPermissionName": null,
"CssClass": null,
"Target": null,
"IsDisabled": false,
"ElementId": null,
"ModuleId": "Hr"
},
{
"Id": "635e0daa-53c5-5956-91a4-a17d581a97d7",
"Code": "App.Hr.LeaveRequest",
"DisplayName": "App.Hr.LeaveRequest",
"Order": 1,
"Url": "/admin/list/App.Hr.LeaveRequest",
"Icon": "FcLeave",
"ParentCode": "App.Hr.Leave",
"RequiredPermissionName": "App.Hr.LeaveRequest",
"CssClass": null,
"Target": null,
"IsDisabled": false,
"ElementId": null,
"ModuleId": "Hr"
},
{
"Id": "4a7c4fd7-ce42-5cef-bd64-f2eeb9dd188f",
"Code": "App.Hr.LeaveBalance",
"DisplayName": "App.Hr.LeaveBalance",
"Order": 2,
"Url": "/admin/list/App.Hr.LeaveBalance",
"Icon": "FcComboChart",
"ParentCode": "App.Hr.Leave",
"RequiredPermissionName": "App.Hr.LeaveBalance",
"CssClass": null,
"Target": null,
"IsDisabled": false,
"ElementId": null,
"ModuleId": "Hr"
}
]
}

View file

@ -113,6 +113,46 @@
"IsActive": true,
"IsFixed": false,
"IsCustomized": false
},
{
"Id": "6da9cf5c-1b30-5905-977a-5a27410c63d8",
"NotificationTypeId": "92cb441c-3ce9-5e6d-a34e-d281260374c3",
"RecipientType": "Custom",
"RecipientId": null,
"Channel": "UiActivity",
"IsActive": true,
"IsFixed": false,
"IsCustomized": false
},
{
"Id": "de9aa39f-c642-5886-9638-8149125cddc1",
"NotificationTypeId": "92cb441c-3ce9-5e6d-a34e-d281260374c3",
"RecipientType": "Custom",
"RecipientId": null,
"Channel": "UiToast",
"IsActive": true,
"IsFixed": false,
"IsCustomized": false
},
{
"Id": "ced331bb-92a9-5f72-b311-3e1a185852ac",
"NotificationTypeId": "7ea2271f-e71c-5b63-8826-9df5e632eb4b",
"RecipientType": "Custom",
"RecipientId": null,
"Channel": "UiActivity",
"IsActive": true,
"IsFixed": false,
"IsCustomized": false
},
{
"Id": "4b1d2769-7073-550d-85a9-b391668bf51e",
"NotificationTypeId": "7ea2271f-e71c-5b63-8826-9df5e632eb4b",
"RecipientType": "Custom",
"RecipientId": null,
"Channel": "UiToast",
"IsActive": true,
"IsFixed": false,
"IsCustomized": false
}
]
}

View file

@ -43,6 +43,16 @@
"Id": "cd6e4aed-f460-5af3-ab18-c3baa33a0a03",
"Name": "Performans360Talebi",
"Description": "Bir değerlendirmeye 360 derece değerlendirici eklendiğinde değerlendiriciye gider. Alıcı geri bildirim kaydındaki değerlendiricidir; bu tipin Custom kuralları yalnızca kanalı belirler (Hr_S_PerformanceFeedbackOpen yazar)."
},
{
"Id": "92cb441c-3ce9-5e6d-a34e-d281260374c3",
"Name": "IzinTalebiOnayBekliyor",
"Description": "İzin talebi müdür onayına ya da İK onayına geldiğinde sıradaki onaycıya gider (müdür: çalışanın departman müdürü, İK: İzin Talepleri akışının İK Onayı düğümü). Alıcı talepten gelir; bu tipin Custom kuralları yalnızca kanalı belirler (Hr_S_LeaveRequestAfterSave yazar)."
},
{
"Id": "7ea2271f-e71c-5b63-8826-9df5e632eb4b",
"Name": "IzinTalebiSonuclandi",
"Description": "İzin talebi onaylandığında ya da reddedildiğinde talep sahibi çalışana gider. Alıcı talepten gelir; bu tipin Custom kuralları yalnızca kanalı belirler (Hr_S_LeaveRequestAfterSave yazar)."
}
]
}

View file

@ -1226,6 +1226,193 @@
"IsVisibleToClients": false,
"IsInherited": false,
"IsEncrypted": false
},
{
"Id": "cd6af2de-ef79-5a19-a6c0-5f4a55049d19",
"Code": "App.HrSettings.Leave.WorkMonday",
"MainGroupKey": "App.HrSettings",
"SubGroupKey": "App.HrSettings.Leave",
"Order": 94,
"NameKey": "App.HrSettings.Leave.WorkMonday",
"DescriptionKey": "App.HrSettings.Leave.WorkDay.Description",
"RequiredPermissionName": "App.HrSettings.Leave",
"Providers": "T|G|D",
"DefaultValue": "True",
"DataType": "Bool",
"SelectOptions": "{}",
"IsVisibleToClients": false,
"IsInherited": false,
"IsEncrypted": false
},
{
"Id": "87448fd3-70d9-509c-9834-fc2e3f90bb70",
"Code": "App.HrSettings.Leave.WorkTuesday",
"MainGroupKey": "App.HrSettings",
"SubGroupKey": "App.HrSettings.Leave",
"Order": 95,
"NameKey": "App.HrSettings.Leave.WorkTuesday",
"DescriptionKey": "App.HrSettings.Leave.WorkDay.Description",
"RequiredPermissionName": "App.HrSettings.Leave",
"Providers": "T|G|D",
"DefaultValue": "True",
"DataType": "Bool",
"SelectOptions": "{}",
"IsVisibleToClients": false,
"IsInherited": false,
"IsEncrypted": false
},
{
"Id": "12157e82-3279-5749-a707-d9eca903ab58",
"Code": "App.HrSettings.Leave.WorkWednesday",
"MainGroupKey": "App.HrSettings",
"SubGroupKey": "App.HrSettings.Leave",
"Order": 96,
"NameKey": "App.HrSettings.Leave.WorkWednesday",
"DescriptionKey": "App.HrSettings.Leave.WorkDay.Description",
"RequiredPermissionName": "App.HrSettings.Leave",
"Providers": "T|G|D",
"DefaultValue": "True",
"DataType": "Bool",
"SelectOptions": "{}",
"IsVisibleToClients": false,
"IsInherited": false,
"IsEncrypted": false
},
{
"Id": "78398216-592a-5c8c-bedb-96728413c324",
"Code": "App.HrSettings.Leave.WorkThursday",
"MainGroupKey": "App.HrSettings",
"SubGroupKey": "App.HrSettings.Leave",
"Order": 97,
"NameKey": "App.HrSettings.Leave.WorkThursday",
"DescriptionKey": "App.HrSettings.Leave.WorkDay.Description",
"RequiredPermissionName": "App.HrSettings.Leave",
"Providers": "T|G|D",
"DefaultValue": "True",
"DataType": "Bool",
"SelectOptions": "{}",
"IsVisibleToClients": false,
"IsInherited": false,
"IsEncrypted": false
},
{
"Id": "9280bf63-43c3-5068-8b2e-18029b5da547",
"Code": "App.HrSettings.Leave.WorkFriday",
"MainGroupKey": "App.HrSettings",
"SubGroupKey": "App.HrSettings.Leave",
"Order": 98,
"NameKey": "App.HrSettings.Leave.WorkFriday",
"DescriptionKey": "App.HrSettings.Leave.WorkDay.Description",
"RequiredPermissionName": "App.HrSettings.Leave",
"Providers": "T|G|D",
"DefaultValue": "True",
"DataType": "Bool",
"SelectOptions": "{}",
"IsVisibleToClients": false,
"IsInherited": false,
"IsEncrypted": false
},
{
"Id": "667c825b-e7d2-5c63-9aa3-8e7dba200d69",
"Code": "App.HrSettings.Leave.WorkSaturday",
"MainGroupKey": "App.HrSettings",
"SubGroupKey": "App.HrSettings.Leave",
"Order": 99,
"NameKey": "App.HrSettings.Leave.WorkSaturday",
"DescriptionKey": "App.HrSettings.Leave.WorkDay.Description",
"RequiredPermissionName": "App.HrSettings.Leave",
"Providers": "T|G|D",
"DefaultValue": "False",
"DataType": "Bool",
"SelectOptions": "{}",
"IsVisibleToClients": false,
"IsInherited": false,
"IsEncrypted": false
},
{
"Id": "8e4f2daa-771f-54e9-9d2e-01bb2a06c4a9",
"Code": "App.HrSettings.Leave.WorkSunday",
"MainGroupKey": "App.HrSettings",
"SubGroupKey": "App.HrSettings.Leave",
"Order": 100,
"NameKey": "App.HrSettings.Leave.WorkSunday",
"DescriptionKey": "App.HrSettings.Leave.WorkDay.Description",
"RequiredPermissionName": "App.HrSettings.Leave",
"Providers": "T|G|D",
"DefaultValue": "False",
"DataType": "Bool",
"SelectOptions": "{}",
"IsVisibleToClients": false,
"IsInherited": false,
"IsEncrypted": false
},
{
"Id": "efae6640-7a65-5c87-baeb-a1c019248c89",
"Code": "App.HrSettings.Leave.AnnualDays1To5",
"MainGroupKey": "App.HrSettings",
"SubGroupKey": "App.HrSettings.Leave",
"Order": 101,
"NameKey": "App.HrSettings.Leave.AnnualDays1To5",
"DescriptionKey": "App.HrSettings.Leave.AnnualDays1To5.Description",
"RequiredPermissionName": "App.HrSettings.Leave",
"Providers": "T|G|D",
"DefaultValue": "14",
"DataType": "Number",
"SelectOptions": "{}",
"IsVisibleToClients": false,
"IsInherited": false,
"IsEncrypted": false
},
{
"Id": "56518255-d95f-51c5-a5a5-aeb079f98059",
"Code": "App.HrSettings.Leave.AnnualDays5To15",
"MainGroupKey": "App.HrSettings",
"SubGroupKey": "App.HrSettings.Leave",
"Order": 102,
"NameKey": "App.HrSettings.Leave.AnnualDays5To15",
"DescriptionKey": "App.HrSettings.Leave.AnnualDays5To15.Description",
"RequiredPermissionName": "App.HrSettings.Leave",
"Providers": "T|G|D",
"DefaultValue": "20",
"DataType": "Number",
"SelectOptions": "{}",
"IsVisibleToClients": false,
"IsInherited": false,
"IsEncrypted": false
},
{
"Id": "3c2df0c4-e64b-5264-95f6-d35208e90444",
"Code": "App.HrSettings.Leave.AnnualDays15Plus",
"MainGroupKey": "App.HrSettings",
"SubGroupKey": "App.HrSettings.Leave",
"Order": 103,
"NameKey": "App.HrSettings.Leave.AnnualDays15Plus",
"DescriptionKey": "App.HrSettings.Leave.AnnualDays15Plus.Description",
"RequiredPermissionName": "App.HrSettings.Leave",
"Providers": "T|G|D",
"DefaultValue": "26",
"DataType": "Number",
"SelectOptions": "{}",
"IsVisibleToClients": false,
"IsInherited": false,
"IsEncrypted": false
},
{
"Id": "649eb63e-98a7-5fcc-85c5-082580d4825c",
"Code": "App.HrSettings.Leave.MinDaysYoungOld",
"MainGroupKey": "App.HrSettings",
"SubGroupKey": "App.HrSettings.Leave",
"Order": 104,
"NameKey": "App.HrSettings.Leave.MinDaysYoungOld",
"DescriptionKey": "App.HrSettings.Leave.MinDaysYoungOld.Description",
"RequiredPermissionName": "App.HrSettings.Leave",
"Providers": "T|G|D",
"DefaultValue": "20",
"DataType": "Number",
"SelectOptions": "{}",
"IsVisibleToClients": false,
"IsInherited": false,
"IsEncrypted": false
}
]
}

View file

@ -63,7 +63,7 @@ Hepsi Grid, Card, Pivot, Chart görünümlüdür.
| Hastaneler | Kurum adı*, aktif | Toplam · Aktif · Pasif | 45 |
| Hastane Bölümleri | Bölüm adı*, aktif | Toplam · Aktif · Pasif | 31 |
| Evrak Türleri | Dosya grubu* (Genel / Özlük Evrakları / Sağlık), dosya türü*, zorunlu, aktif | Toplam · Aktif · Pasif · Zorunlu | 48 |
| İzin Türleri | Kod*, ad*, cinsiyet* (All/Male/Female), yıllık en fazla gün, ücretli, yıllık izinden düşer, belge gerekir, yarım gün kullanılabilir, aktif | Toplam · Ücretli · Ücretsiz | 9 (Yıllık, Mazeret, Ücretsiz, Hastalık, Doğum 112 gün, Babalık 5, Evlilik 3, Ölüm 3, Süt) |
| İzin Türleri | Kod*, ad*, cinsiyet* (All/Male/Female), yıllık en fazla gün, ücretli, yıllık izinden düşer, belge gerekir, yarım gün kullanılabilir, bakiye aşılırsa (Engelle/Uyar), aktif | Toplam · Ücretli · Ücretsiz | 9 (Yıllık, Mazeret, Ücretsiz, Hastalık, Doğum 112 gün, Babalık 5, Evlilik 3, Ölüm 3, Süt) |
| Masraf Türleri | Kod*, ad*, varsayılan tutar, para birimi, gelir vergisine tabi, SGK'ya tabi, fiş/belge gerekir, aktif | Toplam · Aktif · Pasif | 7 (yemek, yol, konaklama, akaryakıt, telefon, temsil, kırtasiye) |
| Yan Hak Türleri | Kod*, ad*, varsayılan tutar, para birimi, gelir vergisine tabi, SGK'ya tabi, aktif | Toplam · Aktif · Pasif | 10 (yemek kartı, ulaşım, özel sağlık sigortası, BES, konut, şirket aracı, telefon, çocuk, yakacak, diğer) |
@ -103,7 +103,8 @@ lookup olarak okunmaları ekran yetkisi gerektirmez. Hiçbir yetki role otomatik
| Yeterlilik Belgeleri | Kullanıcı detayı › Eğitim ve Yetkinlik (`UserCertificate`), Performans gelişim planı (sertifika aksiyonu) |
| Evrak Türleri | Eğitim dosyaları, Disiplin belgeleri |
| Yan Hak Türleri | İşe Alım › Teklif (`BenefitCodes`, kod ile) |
| Hastaneler, Hastane Bölümleri, İzin Türleri, Masraf Türleri | Henüz kullanan ekran yok |
| İzin Türleri | İzin Yönetimi'nin tür kuralları (cinsiyet, yarım gün, belge, yıllık sınır, yıllık izinden düşme, aşım davranışı); `Sick` kodlu tür Sağlık Kartı istirahatiyle eşlenir — `HrLeave.md` |
| Hastaneler, Hastane Bölümleri, Masraf Türleri | Henüz kullanan ekran yok |
---
@ -269,7 +270,7 @@ lookup olarak okunmaları ekran yetkisi gerektirmez. Hiçbir yetki role otomatik
| Düzenleme | ekle ✓ · düzenle ✓ · sil ✓ · popup |
| Kapsam | Kiracı |
| Widget'lar | `App.Widget.Total`, `App.Widget.Paid`, `App.Widget.Unpaid` |
| Form varsayılanları | IsPaid=true, DeductsFromAnnualLeave=false, RequiresDocument=false, AllowHalfDay=false, Gender=All, IsActive=true |
| Form varsayılanları | IsPaid=true, DeductsFromAnnualLeave=false, RequiresDocument=false, AllowHalfDay=false, Gender=All, IsActive=true, BalanceExceedAction=Block |
| Veri aynası | `data/App.Hr.LeaveType.json` |
| Alan | Başlık | Editör | Zorunlu | Formda | Kaynak |
@ -282,6 +283,7 @@ lookup olarak okunmaları ekran yetkisi gerektirmez. Hiçbir yetki role otomatik
| `DeductsFromAnnualLeave` | Yıllık İzinden Düşer | dxCheckBox | ✓ | ✓ | |
| `RequiresDocument` | Belge Gerekir | dxCheckBox | ✓ | ✓ | |
| `AllowHalfDay` | Yarım Gün Kullanılabilir | dxCheckBox | ✓ | ✓ | |
| `BalanceExceedAction` | Bakiye Aşılırsa | dxRadioGroup | ✓ | ✓ | liste: Block, Warn |
| `IsActive` | Aktif | dxCheckBox | ✓ | ✓ | |
### Masraf Türleri — `App.Hr.ExpenseType`

View file

@ -55,7 +55,7 @@ Kapsam: kiracı. Liste, kiracının **tüm kullanıcılarıdır** (aktif/pasif);
| Sonuç | `App.Hr.HealthCardResult` | `AbpUsers` | `Id → Id` | Yalnız güncelleme |
| Periyodik Muayeneler | `App.Hr.HealthCardPeriodicExamination` | `Adm_T_UserPeriodicExamination` | `Id → UserId` | Ekle/düzenle/sil |
| Hasta Protokol Defteri | `App.Hr.HealthCardPatientProtocol` | `Adm_T_UserPatientProtocol` | `Id → UserId` | Ekle/düzenle/sil |
| İstirahat Raporları | `App.Hr.HealthCardSickLeave` | `Adm_T_UserSickLeave` | `Id → UserId` | Ekle/düzenle/sil |
| İstirahat Raporları | `App.Hr.HealthCardSickLeave` | `Adm_T_UserSickLeave` | `Id → UserId` | Ekle/düzenle/sil; kayıt sonrası İzin Yönetimi'ne eşlenir (`HrLeave.md` §5.5) |
Widget'lar (liste): Toplam · Aktif · Pasif kullanıcı.
@ -147,7 +147,7 @@ serbest metindir). Bildirim, periyodik muayene hatırlatması ya da rapor şablo
**Hastane tanımı nerede kullanılıyor?** Şu an hiçbir yerde; ileride istirahat/sevk alanlarına lookup olarak bağlanmak üzere duruyor.
**İstirahat raporu izin bakiyesine yansıyor mu?** Hayır; izin modülü yoktur (İzin Türleri yalnız tanımdır).
**İstirahat raporu izinlere yansıyor mu?** Evet, tek kayıttır: buradan (ya da kullanıcı detayındaki İstirahatler'den) girilen rapor İK › İzin Yönetimi'nde onaylı Rapor (`Sick`) talebi olur; çalışanın Intranet'ten açtığı Rapor talebi de bu sekmeye düşer (`Hr_S_LeaveSickLeaveSync`, ayrıntı `HrLeave.md` §5.5). Rapor yıllık izin bakiyesinden düşmez.
---
@ -414,6 +414,7 @@ entity/migration'a bağlı olduğu için kalır.
| Görünümler | Grid, Card, Pivot, Chart (açılış grid) |
| Düzenleme | ekle ✓ · düzenle ✓ · sil ✓ · popup |
| Kapsam | Kiracı |
| Komutlar | InsertAfter: `Hr_S_LeaveSickLeaveSync` · UpdateAfter: `Hr_S_LeaveSickLeaveSync` · DeleteAfter: SQL |
| Alan | Başlık | Editör | Zorunlu | Formda | Kaynak |
| --- | --- | --- | --- | --- | --- |

View file

@ -74,6 +74,8 @@ Pano kutuları (seed):
| `today-birthdays` | Sol / 10 | `AbpIdentity.Users.Widget` | Bugün doğanlar: avatar, ad, yaş, departman |
| `documents` | Sol / 20 | `App.Files.Widget` | Yayındaki ilk 3 doküman (tür, boyut, tarih, indir) |
| `active-surveys` | Sol / 30 | `App.Hr.Intranet.Survey.Widget` | Aktif anketler (kalan gün, soru/yanıt sayısı, doldurdum/doldur) |
| `my-leave` | Sol / 35 | `App.Hr.MyLeaveRequest` | Custom `MyLeaveWidget`: izin bakiyesi, süren talepler → İzin Talebi Oluştur · Taleplerim · Bakiyem |
| `team-leave` | Sol / 36 | `App.Hr.TeamLeaveRequest` | Custom `TeamLeaveWidget`: müdürün onayını bekleyen izin talepleri, bugün izinde olanlar → Onay Bekleyenler |
| `social-wall` | Orta / 40 | `App.Hr.Intranet.SocialPost.Widget` | Sosyal duvar (Tümü / Paylaşımlarım, sonsuz kaydırma 10'arlı) |
| `announcements` | Sağ / 50 | `App.Hr.Intranet.Announcement.Widget` | Sabitlenmiş duyurular (en fazla 3) |
| `upcoming-events` | Sağ / 60 | `App.Hr.Intranet.Events.Event.Widget` | Bugünden sonraki yayınlanmış ilk 3 etkinlik |
@ -81,7 +83,7 @@ Pano kutuları (seed):
| `team-performance` | Sağ / 80 | `App.Hr.TeamPerformanceReview` | Custom `TeamPerformanceWidget`: yöneticinin ekip değerlendirmeleri → Ekibimin Değerlendirmeleri |
| `my-360-feedback` | Sağ / 90 | `App.Hr.My360Feedback` | Custom `My360FeedbackWidget`: kullanıcıya atanan 360 talepleri → 360 Taleplerim |
İlk altı kutu Builtin'dir (koddaki `Code`). Son üçü Performans Yönetimi'nin çalışana ait özetleridir:
İlk altı kutu Builtin'dir (koddaki `Code`). `my-leave` / `team-leave` İzin Yönetimi'nin (`HrLeave.md` §3.3), son üçü Performans Yönetimi'nin çalışana ait özetleridir:
İK menüsü İK departmanınındır, çalışan kendi değerlendirmesini buradan takip eder; listeler menüsüzdür ve
kutudaki bağlantıyla açılır. Ayrıntı: `HrPerformance.md` §3.3.

View file

@ -0,0 +1,586 @@
# İK › İzin Yönetimi (`App.Hr.Leave`)
İzin talebi, iki aşamalı onay (departman müdürü → İK), yıllık izin bakiyesi ve rapor (istirahat)
eşlemesi. **İnsan Kaynakları menüsü yalnız İK içindir**: tüm talepleri ve bakiyeleri İK izler.
Çalışan ve müdür izinle ilgili her şeyi **Intranet › Dashboard**'daki widget'lardan yapar. Wizard
ekranları, SQL nesneleri ve custom component widget'larıyla kuruludur. Çerçeve dışında iki ek
vardır (kullanıcı onayıyla): Grid Workflow'da kayıttan okunan onaycı (`@Kolon`) ve kullanıcı
detayındaki **İzinler** sekmesi.
> Bu doküman modülün tek "nasıl çalışır" kaynağıdır. Modülde yapılan her değişiklik aynı commit'te
> buraya yansır (`lowcode.instructions.md` §0.13).
---
## 1. Amaç ve kapsam
| Persona | Ne yapar | Ekran |
| --- | --- | --- |
| Çalışan | Bakiyesini izler, izin talebi açar, taleplerinin onay durumunu takip eder, henüz karar verilmemiş talebini düzeltir/siler | Intranet panosu: **İzinlerim** widget'ı → İzin Talebi Oluştur · Taleplerim · Bakiyem |
| Departman müdürü | Departmanının taleplerini onaylar/reddeder | Intranet panosu: **Ekibimin İzinleri** widget'ı → Ekibimin İzin Talepleri |
| İK | Tüm talepleri izler, ikinci onayı verir, çalışan adına talep girer, bakiyeleri izler, açılış/devir düzeltmesi girer, çalışma günlerini ve kademeleri ayarlar | İK › İzin Yönetimi › İzin Talepleri · İzin Bakiyeleri · Ayarlar › İnsan Kaynakları › İzin · Kullanıcı detayı › İzinler |
| Sağlık birimi | Raporu Sağlık Kartı'na girer; kayıt izin listesine onaylı düşer | İK › Sağlık Kartları › İstirahat Raporları |
Kapsam: kiracı (`IsTenant`). Şube kırılımı yoktur.
---
## 2. Analiz ve kararlar
### 2.1 Talep
> İnsan Kaynakları › İzin Yönetimi. Kullanıcı Intranet Dashboard'dan kendi bakiye raporunu izler,
> izin talebi oluşturur, bir butonla taleplerini ve onay durumlarını takip eder (Performansım
> widget'ındaki Değerlendirmelerim gibi). İzin türüne göre gerekli alanlar doldurulur; bakiye
> yetmezse talep engellenir veya uyarı verilir; aynı tarihlerde çakışan talep oluşturulamaz.
> Sağlık Kartı › İstirahatler ile Rapor türündeki izin tek kayıttır. İzin bakiyesi kullanıcı
> kartında gösterilir. Alanlar: izin türü, başlangıç/bitiş tarihi ve saati, yarım gün, gün sayısı
> (otomatik), açıklama, ek belge, durum. Yıllık hak çalışma yılına göre hesaplanır. Onay önce
> departman müdürü (İş Pozisyonları'ndaki departman müdürü), sonra İK. Talep gelince yöneticiye
> bildirim gider. İK menüsü yalnız İK kullanır; widget'lar Aktif Anketler'in altına (sol kolon).
### 2.2 Depodan okunanlar (sorulmadı)
- İzin türleri `Hr_T_LeaveType` (İK › Tanımlamalar): cinsiyet, yıllık en fazla gün, ücretli, yıllık izinden düşer, belge gerekir, yarım gün.
- Rapor kayıtları `Adm_T_UserSickLeave`; iki ekran kullanır: Sağlık Kartı (`App.Hr.HealthCardSickLeave`) ve kullanıcı detayı (`App.IdentityManagement.UserSickLeave`).
- Kıdem `AbpUsers.SeniorityStartDate` (boşsa `HireDate`), doğum `BirthDate`, cinsiyet `Gender`, departman/pozisyon `DepartmentId`/`JobPositionId`.
- Tatiller `Hr_T_WorkCalendar` (`Holiday` / `HalfDay`, `FREQ=YEARLY` tekrar).
- Pozisyon ağacı `Hr_T_JobPosition` (`DepartmentId` + `ParentId`); departmanda "müdür" alanı yoktur.
- Grid Workflow onaycıyı düğümde sabit tutuyordu — kayda göre değişen müdür için motor eki gerekti.
### 2.3 Keşif turu cevapları
| # | Soru | Seçilen |
| --- | --- | --- |
| 1 | Onay akışı | Grid Workflow + motor eki: düğüm onaycısı `@Kolon` ise kayıttan okunur |
| 2 | Müdür nasıl bulunur | Departmanın en üst pozisyonu (üst pozisyonu başka departmanda ya da üstü olmayan); çalışan o pozisyondaysa bir üst pozisyonun sahibi; birden çok müdür varsa herhangi biri |
| 3 | Yıllık hak | İş Kanunu md.53 kademeleri + devir (kullanılmayan izin devreder) |
| 4 | Bakiye yetersiz | İzin türü bazında İK seçer: `BalanceExceedAction` = Engelle / Uyar ve kaydet |
| 5 | İK onaycısı | Düğümde sabit kullanıcı(lar) (İzin Talepleri akışının "İK Onayı" düğümü) |
| 6 | Gün sayısı | Ayarlar › İnsan Kaynakları › İzin'deki çalışma günleri + Çalışma Takvimi tatilleri (kullanıcı kararı: tüm İK ayarları bu grupta) |
| 7 | Rapor talebi | Onay akışından geçer; kayıt anında Sağlık Kartı'nda görünür, reddedilirse kalkar |
| 8 | Kullanıcı kartı | Kullanıcı detayına **İzinler** sekmesi (bakiye özeti + talepler + defter) |
| 9 | Üstü olmayan pozisyon (Genel Müdür) | Müdür adımı atlanır, talep doğrudan İK Onayı'na gider (üretim sırasında soruldu) |
### 2.4 Üretimde verilen kararlar
| Konu | Karar | Neden |
| --- | --- | --- |
| Persona ekranında akış | **İzin Taleplerim** ekranında Workflow yoktur; ilk adımı (`Müdür Onayı` + müdür kullanıcı adları) kayıt sonrası prosedür yazar | Workflow alanlarını yazabilen persona ekranı, çalışanın durumu API ile "Onaylandı" yapmasına izin verirdi; alanlar İK yetkisine kilitli |
| Müdür ekranı | **Ekibimin İzin Talepleri** ayrı ekran, aynı akış başlıklarıyla; satır `ManagerIds` ile süzülür | Akış düğümleri ekrana bağlıdır, durum başlıkla eşleşir (`FindCurrentCriteria`) |
| İK onaycısının tek kaynağı | Kayıt sonrası prosedür İK adımına girerken onaycıyı **İzin Talepleri** akışının "İK Onayı" düğümünden okur | Ekibimin İzin Talepleri akışındaki İK düğümü ayrı düzenlenirse sapma olmasın |
| Yöneticinin geçişleri | Müdür yalnız Müdür Onayı → İK Onayı / Reddedildi yapabilir; başka geçiş geri alınır | Workflow alanları müdür ekranında yazılabilir olmak zorunda (motor ekranın kayıt yolunu kullanır) |
| Bakiye defteri | Hak ediş yılı başına satır; 0. yıl düzeltmeler ve ilk hak edişten önceki kullanım | Devirli kümülatif bakiyenin denetlenebilir dökümü |
| Açılış bakiyesi | **Bakiye Düzeltmeleri** (± gün) | Devirli hesapta sistem öncesi kullanım girilmezse geçmiş bütün yıllar kullanılmamış görünür |
| Bildirim | Uygulama içi + toast (`IzinTalebiOnayBekliyor`, `IzinTalebiSonuclandi`); Workflow `Inform` düğümü kullanılmadı | Inform yalnız e-posta gönderir ve onaycısı sabittir |
| Ek belge | `dxImageUpload` (base64, `nvarchar(MAX)`), listede gizli | Platform standardı |
| En üst pozisyon | Prosedür `RequiresManagerApproval`'ı yazar (1 = müdür onayı gerekir, 0 = atlanır); İzin Talepleri akışında Başlangıç ile Müdür Onayı arasında bu kolona bakan karşılaştırma düğümü vardır, çalışanın kendi talebinde prosedür durumu doğrudan İK Onayı yapar | Motorun karşılaştırma düğümü yalnız sayısal kolon okur (`EvaluateCondition`) ve satırı ekranın sorgusundan alır; kolon İzin Talepleri'nde gizli alandır |
| Form günü | Gün formda tarayıcıda önizlenir (hafta içi gün, yarım günde 0,5), kalıcı değer prosedürün hesabıdır; İzin Türü lookup'ı cascade değildir | Cascade ebeveyni çocuğu kilitler ve değiştiğinde temizler; ayrıntı §5.1 |
| Bildirim başlığı | Bildirim panelinde ve etkinlik kaydında tip adı yerine kaydın `Subject`'i gösterilir (bütün modüller) | Tip adı (`PerformansDurumDegisti`) koddur; konu çevrilmiş metindir |
### 2.5 Varsayılan kalanlar
- Çalışma günleri Pzt–Cum; kademeler 14 / 20 / 26, genç/yaşlı en az 20 (ayarlarda değiştirilir).
- İzin türü varsayılanı: yıllık izinden düşen ya da yıllık sınırı olan türler **Engelle**, diğerleri **Uyar ve kaydet** (seed).
- Rapor kaydı `SickLeaveType = Disease`, gün sayısı takvim günü (Sağlık Kartı kuralı); izin talebindeki gün çalışma günüdür.
---
## 3. Menü ve ekranlar
### 3.1 İK menüsü
`İnsan Kaynakları › İzin Yönetimi` (`App.Hr.Leave`, sıra 14, `FcOvertime`) — yalnız İK.
| Sıra | Menü (TR / EN) | `ListFormCode` | Wizard | Kaynak | Alt formlar | Widget'lar |
| --- | --- | --- | --- | --- | --- | --- |
| 1 | İzin Talepleri / Leave Requests | `App.Hr.LeaveRequest` | `LeaveRequest.json` | `Hr_T_LeaveRequest` | — | Müdür Onayında · İK Onayında · Onaylanan (bu yıl) · Reddedilen (bu yıl) |
| 2 | İzin Bakiyeleri / Leave Balances | `App.Hr.LeaveBalance` | `LeaveBalance.json` | `Hr_V_LeaveSummary` (salt okunur) | `LeaveBalanceAdjustment`, `LeaveBalanceYear` (`Id → EmployeeId`) | Çalışan · Toplam Kalan Gün · Eksi Bakiye · Onay Bekleyen Gün |
Menüsüz alt formlar: **Bakiye Düzeltmeleri** (`App.Hr.LeaveBalanceAdjustment`, `Hr_T_LeaveBalanceAdjustment`),
**İzin Defteri** (`App.Hr.LeaveBalanceYear`, `Hr_V_LeaveBalanceYear`, salt okunur).
Görünümler: hepsi Grid, Card, Pivot, Chart.
### 3.2 Çalışana ve müdüre ait ekranlar (menüsüz)
| Ekran | `ListFormCode` | Satır süzmesi | Yapılabilen |
| --- | --- | --- | --- |
| İzin Taleplerim | `App.Hr.MyLeaveRequest` | `EmployeeId = @USERID` | Ekle; karar verilmeden düzelt/sil. `EmployeeId` sunucuda `@USERID` ile yazılır |
| Bakiyem | `App.Hr.MyLeaveBalance` | `EmployeeId = @USERID` | Defteri görür |
| Ekibimin İzin Talepleri | `App.Hr.TeamLeaveRequest` | `ManagerIds` içinde kullanıcı | Müdür adımında Onayla / Reddet |
Talep formu doğrudan açılır: `/admin/form/App.Hr.MyLeaveRequest`.
### 3.3 Intranet › Dashboard widget'ları
Sol kolon (`Column: 0`), Aktif Anketler'in (30) altında; kayıtlar `data/App.DeveloperKit.IntranetWidgets.json`.
| `Code` · sıra | Bileşen | `PermissionName` | Gösterir | Butonlar |
| --- | --- | --- | --- | --- |
| `my-leave` · 35 | `MyLeaveWidget` | `App.Hr.MyLeaveRequest` | Kalan / kullanılan / onay bekleyen gün, sonraki hak ediş, süren ilk 3 talep ve durumu | İzin Talebi Oluştur · Taleplerim · Bakiyem |
| `team-leave` · 36 | `TeamLeaveWidget` | `App.Hr.TeamLeaveRequest` | Onayımı bekleyen talep sayısı ve ilk 5'i, bugün izinde olanlar | Onay Bekleyenler |
Bileşenler code modunda, rotasızdır; veriyi ekranların kendi sorgusundan okur
(`GET /api/app/list-form-select/select`). Bakiye kartları `App.Hr.MyLeaveBalance` yetkisi yoksa gizlenir.
### 3.4 Kullanıcı detayı › İzinler
`ui/src/views/admin/user-management/LeaveTab.tsx`: İzin Bakiyeleri satırından altı kart (hak edilen,
düzeltme, kullanılan, bekleyen, kalan, kullanılabilir), altında kullanıcıya süzülmüş **İzin Talepleri**
ve **İzin Defteri** listeleri. Her parça kendi ekran yetkisiyle görünür; İK buradan çalışan adına talep
ekleyebilir (yeni satırın `EmployeeId`'si süzgeçten dolar).
### 3.5 Ayarlar
Ayarlar › İnsan Kaynakları › İzin (`App.HrSettings` › `App.HrSettings.Leave`, sağlayıcılar `T|G|D`):
| Kod | Varsayılan |
| --- | --- |
| `App.HrSettings.Leave.Work{Monday…Sunday}` | Pzt–Cum `True`, Cmt–Paz `False` |
| `App.HrSettings.Leave.AnnualDays1To5` / `AnnualDays5To15` / `AnnualDays15Plus` | 14 / 20 / 26 |
| `App.HrSettings.Leave.MinDaysYoungOld` | 20 |
SQL tarafı ayarı `dbo.Hr_F_HrSetting` ile ABP'nin sırasıyla okur (kiracı → genel → tanım varsayılanı).
---
## 4. Veri modeli
```
AbpUsers ──< Hr_T_LeaveRequest >── Hr_T_LeaveType
│ └── Adm_T_UserSickLeave (SickLeaveId, Rapor türünde)
└──< Hr_T_LeaveBalanceAdjustment
Hr_V_LeaveBalanceYear (defter) ──▶ Hr_V_LeaveSummary (çalışan başına toplam)
```
| Nesne | Önemli kolonlar | Kural |
| --- | --- | --- |
| `Hr_T_LeaveRequest` | `RequestNo`, `EmployeeId`/`EmployeeName`/`DepartmentId`/`JobPositionId`, `LeaveTypeId`, `StartDate`/`EndDate` (tarih+saat), `IsHalfDay`, `HalfDayPart` (Morning/Afternoon), `Days` (0,5 adımlı), `Description`, `DocumentFileName`/`DocumentContent`, `BalanceWarning`, `SickLeaveId`, `ManagerIds`/`ManagerUserNames`, `RequiresManagerApproval` (int, 1/0), `ApprovalStatus`/`ApprovalUser`/`ApprovalDate`/`ApprovalDescription`, `LastNotifiedStatus`, `IsActive` | `EndDate ≥ StartDate`; kiracı + tam audit |
| `Hr_T_LeaveBalanceAdjustment` | `EmployeeId`, `AdjustmentDate`, `Days` (≠ 0, ±), `Reason`, `IsActive` | — |
| `Hr_T_LeaveType` (+ kolon) | `BalanceExceedAction` (`Block`/`Warn`, varsayılan `Block`) | Kayıpsız `ALTER ADD` |
| `Hr_V_LeaveBalanceYear` | `Id` (`{EmployeeId}-{YearNo}`), `YearNo`, `AccrualDate`, `PeriodEndDate`, `EntitledDays`, `UsedDays`, `PendingDays`, `RemainingDays`, `CumulativeRemainingDays`, `NextAccrualDate`, `NextAccrualDays` | Görünüm |
| `Hr_V_LeaveSummary` | `Id` (= kullanıcı), `ServiceYears`, `EntitledDays`, `AdjustmentDays`, `UsedDays`, `PendingDays`, `RemainingDays`, `AvailableDays`, `NextAccrualDate/Days`, `IsActive` | Görünüm |
Durum değerleri (Workflow düğüm başlıkları): `Onaya Gönderildi` · `Müdür Onayı` · `İK Onayı` · `Onaylandı` · `Reddedildi`.
---
## 5. İş kuralları ve hesaplar
### 5.1 Gün sayısı (`Hr_F_LeaveWorkDays`)
Başlangıç–bitiş tarihleri (saat dikkate alınmaz) arasındaki her gün: ayarlarda çalışma günü değilse 0;
Çalışma Takvimi'nde `Holiday` ise 0, `HalfDay` ise 0,5; aksi 1. Yıllık tekrarlı satır başlangıç
yılından itibaren her yıl aynı gün/aya düşer. Yarım gün talepte sonuç en fazla 0,5'tir.
Formda gün veri girerken dolar (Başlangıç, Bitiş ve Yarım Gün alanlarının `EditorScript`'i, `custom` tarifi).
Bu bir önizlemedir: iki tarih arasındaki hafta içi günler sayılır, yarım günde 0,5 yazılır; Çalışma Günleri
ayarı ve Çalışma Takvimi tatilleri formda düşülmez. Kayıtta prosedür günü `Hr_F_LeaveWorkDays` ile yeniden
hesaplar ve kayıtlı değer odur.
| Olay | Formda |
| --- | --- |
| Başlangıç / Bitiş değişir | Gün yeniden hesaplanır; yarım gün açıkken Başlangıç değişirse Bitiş onu izler |
| Yarım Gün işaretlenir | Bitiş = Başlangıç, Gün 0,5, Yarım Gün Dilimi açılır |
| Yarım Gün kaldırılır | Dilim temizlenip kilitlenir, Gün tam gün olarak hesaplanır |
Notlar:
- **Dilim kilidi `custom` kuralıdır** (`readOnly` tarifi değil). Grid popup'ında editörler yeniden çizilince
kural açılış olayıyla (`isReady`) tekrar çalışır; o anda form verisi güncel değildir (`form.getEditor` popup
editörünü döndürmez) ve tarif Yarım Gün'ü eski hâliyle okuyup kilidi geri kapatırdı. Kural bu çalışmada
değeri grid'in düzenlenen satırından (`editor.component.cellValue`) okur; form sayfasında form verisinden.
- **Dilimin script'i yoktur.** Form sayfası script'li bir editör değişince `formData`'yı baştan yazar; bu sırada
açılır liste temizlenip yeniden dolar ve script'i olsaydı kendini tetikleyip sonsuz döngüye girerdi.
Tutarlılığı prosedür sağlar: yarım gün değilse dilim silinir, yarım günde boş dilim `Morning` olur.
- **İzin Türü düz bir lookup'tır**, cascade kullanılmaz: bool ebeveyn `false` iken çocuğu kilitler, tarih
değişince değeri silerdi.
### 5.2 Yıllık hak ve bakiye (`Hr_V_LeaveBalanceYear`)
| Değer | Formül |
| --- | --- |
| Hak ediş tarihi | Kıdem başlangıcı + n yıl (n = 1, 2, …), bugün ve (varsa) işten çıkış tarihine kadar |
| Hak edilen | n ≤ 5 → `AnnualDays1To5`; 6 ≤ n ≤ 14 → `AnnualDays5To15`; n ≥ 15 → `AnnualDays15Plus`. Hak ediş tarihindeki yaş ≤ 18 ya da ≥ 50 ise en az `MinDaysYoungOld` |
| Kullanılan / bekleyen | Yıllık izinden düşen türdeki onaylı / onay bekleyen taleplerin günü; talep başlangıcının düştüğü hak ediş dönemine yazılır |
| 0. yıl | Düzeltmelerin toplamı ve ilk hak edişten önce başlayan talepler |
| Kalan | Σ(hak edilen + düzeltme) − Σ kullanılan (devirli) |
| Kullanılabilir | Kalan − onay bekleyen |
### 5.3 Kayıt sonrası (`Hr_S_LeaveRequestAfterSave`)
Her ekleme/güncellemede (Workflow kararı dahil) çalışır; `THROW` kaydı geri alır.
1. Çalışan adı, departman, pozisyon kullanıcı kartından; talep no `IZN-YYYY-00001`. (İK formunda çalışan seçilince departman ve pozisyon formda da hemen dolar: Çalışan lookup'ı iki kolonu döndürür, `copy` tarifi yazar.)
2. Müdür (§2.3 karar 2) bulunur → `ManagerIds`, `ManagerUserNames`. Bulunamazsa: çalışanın pozisyonunun üst pozisyonu yoksa (Genel Müdür) `RequiresManagerApproval = 0` yazılır ve müdür adımı atlanır; değilse ve talep müdür adımına kadar ise `THROW 51110` (yapılandırma eksik).
3. Müdür ekranından izinsiz geçiş önceki duruma geri alınır.
4. Çalışanın yeni talebi `Müdür Onayı`'na (müdür adımı atlanıyorsa `İK Onayı`'na) alınır; adıma girerken onaycı yazılır (müdür adımı → `ManagerUserNames`, İK adımı → İzin Talepleri akışının "İK Onayı" düğümü).
5. Gün sayısı hesaplanır.
6. Talep sonuçlanmamışsa doğrulanır:
| Kod | Kural |
| --- | --- |
| 51111 | Tür cinsiyeti çalışana uymuyor |
| 51112 | Tür yarım güne izin vermiyor |
| 51113 | Yarım günde başlangıç ve bitiş aynı gün olmalı |
| 51114 | Tür belge istiyor, belge yok |
| 51115 | Aralıkta çalışma günü yok |
| 51116 | Reddedilmemiş başka bir talep ya da talebe bağlı olmayan istirahat aynı güne düşüyor (aynı günün iki ayrı yarısı serbest) |
| 51117 | Yıllık izinden düşen türde gün > kullanılabilir bakiye, ya da türün yıllık sınırı aşılıyor — tür `Block` ise; `Warn` ise kayıt yapılır, uyarı `BalanceWarning`'e yazılır |
7. Rapor (`Code = Sick`) türünde istirahat satırı açılır/güncellenir; reddedilen ya da türü değişen talebin istirahati kalkar.
8. Durum bildirim taşıyan bir değere geçtiyse bir kez bildirim gider, `LastNotifiedStatus` güncellenir.
### 5.4 Satır koruması (`Hr_S_LeaveRequestGuard`, before-command)
| Aktör | Kural |
| --- | --- |
| Çalışan | Yalnız kendi talebi (`51100`); durum `Onaya Gönderildi`/`Müdür Onayı` değilse düzeltme/silme yok (`51101`) |
| Müdür | Silemez (`51102`); `ManagerIds` içinde olmalı (`51103`); talep `Müdür Onayı`'nda olmalı (`51104`) |
| İK | Serbest |
Alan kilidi: persona ekranlarında çalışanın/müdürün değiştirmemesi gereken alanların yazma yetkisi
`App.Hr.LeaveRequest.Create/.Update`'e bağlıdır; sunucu yetkisi olmayan alanı düşürür. Bu yüzden
çalışan/müdür rolüne İzin Talepleri'nin `.Create/.Update` yetkisi verilmez.
### 5.5 Rapor ↔ izin (tek kayıt)
| Nereden | Ne olur |
| --- | --- |
| İzin talebi, tür Rapor | `Adm_T_UserSickLeave` satırı açılır (`SickLeaveId`); talep güncellenince güncellenir, silinince/reddedilince kalkar |
| Sağlık Kartı › İstirahat Raporları ya da kullanıcı detayı › İstirahatler | `Hr_S_LeaveSickLeaveSync`: onaylı Rapor talebi açılır (onay akışına girmez), güncellemede tarih/gün, silmede talep kalkar. Kiracıda `Sick` kodlu tür yoksa eşleme yapılmaz |
### 5.6 Onay akışı (Grid Workflow)
`Onaya Gönderildi` (Start) → `Müdür Onayı Gerekli mi?` (Compare, `RequiresManagerApproval = 1`) →
evet: `Müdür Onayı` (Approval, `@ManagerUserNames`) / hayır: doğrudan `İK Onayı` (Approval, seed'de
`system@sozsoft.com`) → `Onaylandı` (End); iki onay adımının reddi → `Reddedildi` (End).
Karşılaştırma düğümü yalnız İzin Talepleri akışındadır (akış İK grid'inden eklenen kayıtta orada başlar);
Ekibimin İzin Talepleri akışı başlatılmaz, yalnız müdür adımından ilerler. Akış iki ekranda aynı başlıklarla durur. İK onaycısı İzin
Talepleri › ListForm › Workflow › "İK Onayı" düğümünden değiştirilir (§2.4).
---
## 6. Yetki ve görünürlük
İzin grubu `App.Hr`, modül `Sas|Hr`. Dal `App.Hr.Leave` → `App.Hr.LeaveRequest`, `App.Hr.LeaveBalance`
(altında `LeaveBalanceAdjustment`, `LeaveBalanceYear`). Kökte menüsüz `App.Hr.MyLeaveRequest`,
`App.Hr.MyLeaveBalance`, `App.Hr.TeamLeaveRequest`. Ayar: `App.HrSettings` › `App.HrSettings.Leave`
(Administration grubu, `App.Setting` altında). Her ekranda `.Create/.Update/.Delete/.Export/.Import/.Note`.
**Hiçbir yetki role otomatik atanmaz.**
| Rol | Verilecek yetkiler |
| --- | --- |
| Tüm çalışanlar | `App.Hr.MyLeaveRequest` (+ `.Create/.Update/.Delete`), `App.Hr.MyLeaveBalance` |
| Müdürler | Yukarıdakiler + `App.Hr.TeamLeaveRequest` (+ `.Update` — Workflow kararı güncelleme yetkisi ister) |
| İK | `App.Hr.Leave` dalının tamamı + `App.HrSettings.Leave` |
Widget'lar liste okuma yetkisiyle görünür (İzinlerim `App.Hr.MyLeaveRequest`, Ekibimin İzinleri `App.Hr.TeamLeaveRequest`).
---
## 7. Entegrasyonlar
| Kaynak | Kullanım |
| --- | --- |
| `AbpUsers` | Çalışan, kıdem, doğum tarihi, cinsiyet, departman/pozisyon; onaycı kullanıcı adları |
| `Hr_T_JobPosition` / `Hr_T_Department` | Departman müdürü çözümü |
| `Hr_T_LeaveType` | Tür kuralları (cinsiyet, yarım gün, belge, yıllık sınır, yıllık izinden düşme, aşım davranışı) |
| `Hr_T_WorkCalendar` | Tatil ve yarım günler |
| `AbpSettings` / `Sas_H_SettingDefinition` | Çalışma günleri ve kademeler |
| `Adm_T_UserSickLeave` | Rapor eşlemesi |
| Bildirimler | `IzinTalebiOnayBekliyor` (müdür / İK onaycısı), `IzinTalebiSonuclandi` (çalışan); kurallar `Custom` + `UiActivity`/`UiToast`. Metinler `App.Hr.Leave.Notification.{Approval,Result}.{Subject,Message}` (TR), yer tutucular `[Employee]`, `[LeaveType]`, `[StartDate]`, `[EndDate]`, `[Days]`, `[Status]`. Yazan: `Hr_S_LeaveNotify` |
---
## 8. Akış örneği
1. İK Ayarlar › İnsan Kaynakları › İzin'de Cumartesi'yi çalışma günü yapar (gerekirse); eski çalışanlar için İzin Bakiyeleri › Bakiye Düzeltmeleri'ne sistem öncesi kullanılan günleri eksi olarak girer.
2. Muhasebe Şefi panodaki **İzinlerim** widget'ında kalan 18 günü görür, **İzin Talebi Oluştur** ile 3 günlük yıllık izin açar.
3. Gün kayıtta 3 hesaplanır, talep `Müdür Onayı`'na düşer, Muhasebe Müdürü bildirim alır.
4. Müdür **Ekibimin İzinleri** → Onay Bekleyenler'den talebi seçip **Onayla** der → talep `İK Onayı`'na geçer, İK onaycısı bildirim alır.
5. İK İzin Talepleri'nde onaylar → `Onaylandı`; çalışan bildirim alır, bakiye 15'e iner.
6. Çalışan rapor alır, Rapor türünde talep açıp raporu ekler → Sağlık Kartı › İstirahat Raporları'nda görünür; aynı onay akışından geçer.
7. Sağlık birimi başka bir raporu doğrudan Sağlık Kartı'na girer → İzin Talepleri'nde onaylı Rapor talebi olarak görünür.
---
## 9. Sık sorulanlar
**"Departman müdürü bulunamadı" hatası.** Çalışanın kullanıcı kartında departman ve pozisyon dolu olmalı; departmanın en üst pozisyonunda (ya da üst pozisyonlarda) aktif bir kullanıcı bulunmalı (§5.3 adım 2). Üst pozisyonu olmayan çalışan (Genel Müdür) bu hatayı almaz; talebi doğrudan İK onayına gider.
**Genel Müdür'ün talebini kim onaylar?** Müdür adımı atlanır, yalnız İK onaylar (§2.3 karar 9).
**Gün sayısı beklenenden az.** Hafta sonu, ayarlarda kapalı gün ve Çalışma Takvimi tatilleri sayılmaz; arife 0,5 sayılır.
**Bakiye çok yüksek görünüyor.** Devirli hesap kıdem başlangıcından bugüne bütün yılları sayar; sistem öncesi kullanılan izin Bakiye Düzeltmeleri'ne eksi olarak girilmelidir.
**Bakiyem/İzinlerim'de kıdem uyarısı.** Kullanıcı kartında Kıdem Başlangıç ya da İşe Giriş tarihi yok.
**Talebimi değiştiremiyorum.** Müdür karar verdikten sonra çalışan değiştiremez/silemez (`51101`); İK ile görüşülür.
**Müdür Onayla butonunu görmüyor.** Rolünde `App.Hr.TeamLeaveRequest.Update` olmalı; talep `Müdür Onayı`'nda ve kullanıcı o departmanın müdürü olmalı.
**İK onaycısı nasıl değişir?** İzin Talepleri › ListForm › Workflow › "İK Onayı" düğümü. Ekibimin İzin Talepleri'ndeki aynı düğüm akışı ilerletir; onaycı kayda İzin Talepleri düğümünden yazılır.
**Bildirim gelmiyor.** Kiracıda `IzinTalebiOnayBekliyor` / `IzinTalebiSonuclandi` tipi ve `Custom` kuralları olmalı (yeni kiracıya kopyalanır, mevcut kiracıda Administration › Bildirimler'den eklenir).
**Kullanıcı detayındaki İstirahatler'den girilen rapor izinlere düşmüyor.** O ekranın kancaları C# seeder'dadır (`ListFormSeeder_Administration`); daha önce kurulmuş bir veritabanında ekran yeniden kurulmadıkça kanca yoktur. Sağlık Kartı ekranı wizard'dır ve migrate ile güncellenir.
---
## 10. Dosyalar ve geri alma
| Dosya | İçerik |
| --- | --- |
| `configs/seeds/host/sql/object/Hr_T_LeaveRequest.sql`, `Hr_T_LeaveBalanceAdjustment.sql`, `Hr_T_LeaveType.sql` (`BalanceExceedAction`) | Tablolar |
| `…/Hr_F_HrSetting.sql`, `Hr_F_LeaveWorkDays.sql` | Ayar okuma, gün sayısı |
| `…/Hr_V_LeaveBalanceYear.sql`, `Hr_V_LeaveSummary.sql` | Bakiye görünümleri |
| `…/Hr_S_LeaveRequestAfterSave.sql`, `Hr_S_LeaveRequestGuard.sql`, `Hr_S_LeaveNotify.sql`, `Hr_S_LeaveSickLeaveSync.sql` | Prosedürler |
| `configs/seeds/host/wizard/{LeaveRequest,MyLeaveRequest,TeamLeaveRequest,LeaveBalance,LeaveBalanceAdjustment,LeaveBalanceYear,MyLeaveBalance}.json` | 7 wizard; `LeaveType.json` (alan), `HealthCardSickLeave.json` (kancalar) |
| `configs/seeds/host/data/App.DeveloperKit.CustomComponents/{23744edc-eda9-512e-8079-fe2e8e5fd2a8,3b7b16c6-aa96-5765-8e19-fe5e727feb52}.json` | `MyLeaveWidget`, `TeamLeaveWidget` |
| `configs/seeds/host/data/App.DeveloperKit.IntranetWidgets.json`, `App.SettingDefinitions.json`, `App.Notifications.Notification{Types,Rules}.json`, `App.Menus.Menu.json`, `Abp.Identity.Permissions.json`, `App.Languages.LanguageText.json`, `App.Hr.LeaveType.json` | Seed satırları |
| `api/src/Sozsoft.Platform.Application/ListForms/ListFormWorkflowAppService.cs` | `@Kolon` onaycı |
| `api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Administration.cs` | Kullanıcı detayı istirahat kancaları |
| `ui/src/views/admin/listForm/workflow/WorkflowCriteria.tsx` | Tasarımcıda alan onaycısı seçeneği |
| `ui/src/views/admin/user-management/LeaveTab.tsx`, `Details.tsx` | İzinler sekmesi |
Kurulum: DB Migrate (SQL nesneleri, ekranlar `WizardDataSeeder`, satırlar `ListFormSeedDataApplier`),
ardından rollere yetki (§6). Ayar tanımları uygulama yeniden başlayınca ayar ekranında görünür.
Geri alma: Wizard Manager'dan 7 wizard silinir (alt formlar önce); tabloları düşürmek onayda seçilirse olur.
Widget bileşenleri ve pano kayıtları, bildirim tip/kural satırları, ayar tanımları, `Hr_F_*`/`Hr_V_*`/`Hr_S_Leave*`
nesneleri elle kaldırılır. `BalanceExceedAction` kolonu kalır (kayıpsız ekleme geri alınmaz).
<!-- module-doc:auto:begin -->
## Ek — Ekran envanteri (otomatik)
> Wizard dosyalarından üretilir; deploy, silme, tasarımcı kaydı ve import'ta yenilenir. Elle düzenleme bir sonraki yenilemede kaybolur.
### İzin Talepleri — `App.Hr.LeaveRequest`
| Özellik | Değer |
| --- | --- |
| Wizard | `wizard/LeaveRequest.json` |
| Yol | Liste (Wizard) |
| Menü | `App.Hr.Leave` › `App.Hr.LeaveRequest` (sıra 1) |
| Yetki | `App.Hr.LeaveRequest` + `.Create`, `.Update`, `.Delete`, `.Export`, `.Import`, `.Note` |
| Açıklama | İzin Talepleri — kurumdaki bütün izin talepleri ve onay durumları. Yalnız İnsan Kaynakları kullanır. |
| Kaynak | `Hr_T_LeaveRequest` · filtre `"IsDeleted" = 'false'` |
| Görünümler | Grid, Card, Pivot, Chart (açılış grid) |
| Düzenleme | ekle ✓ · düzenle ✓ · sil ✓ · popup |
| Kapsam | Kiracı |
| Widget'lar | `App.Hr.Leave.Widget.ManagerApproval`, `App.Hr.Leave.Widget.HrApproval`, `App.Hr.Leave.Widget.Approved`, `App.Hr.Leave.Widget.Rejected` |
| Komutlar | InsertAfter: `Hr_S_LeaveRequestAfterSave` · UpdateAfter: `Hr_S_LeaveRequestAfterSave` · DeleteAfter: SQL |
| Form varsayılanları | IsHalfDay=false, StartDate=@DATE, EndDate=@DATE |
| Onay akışı | Start 'Onaya Gönderildi' → Compare 'Müdür Onayı Gerekli mi?' → Approval 'Müdür Onayı' → Approval 'İK Onayı' → End 'Onaylandı' → End 'Reddedildi' |
| Alan | Başlık | Editör | Zorunlu | Formda | Kaynak |
| --- | --- | --- | --- | --- | --- |
| `RequestNo` | Talep No | dxTextBox | | ✓ | |
| `EmployeeId` | Çalışan | dxSelectBox | ✓ | ✓ | sorgu: AbpUsers · script |
| `DepartmentId` | Departman | dxSelectBox | | ✓ | sorgu: Hr_T_Department |
| `JobPositionId` | İş Pozisyonu | dxSelectBox | | ✓ | sorgu: Hr_T_JobPosition |
| `LeaveTypeId` | İzin Türü | dxSelectBox | ✓ | ✓ | sorgu: Hr_T_LeaveType |
| `StartDate` | Başlangıç Tarihi | dxDateBox | ✓ | ✓ | script |
| `EndDate` | Bitiş Tarihi | dxDateBox | ✓ | ✓ | script |
| `IsHalfDay` | Yarım Gün | dxCheckBox | | ✓ | script |
| `HalfDayPart` | Yarım Gün Dilimi | dxSelectBox | | ✓ | liste: Morning, Afternoon |
| `Days` | Gün | dxNumberBox | | ✓ | |
| `BalanceWarning` | Bakiye Uyarısı | dxTextBox | | ✓ | |
| `ManagerUserNames` | Departman Müdürleri | dxTextBox | | | |
| `RequiresManagerApproval` | Müdür Onayı Gerekli | dxNumberBox | | | |
| `Description` | Açıklama | dxTextArea | | ✓ | |
| `DocumentFileName` | Belge Adı | dxTextBox | | ✓ | |
| `DocumentContent` | Belge (rapor vb.) | dxImageUpload | | ✓ | |
| `ApprovalStatus` | Onay Durumu | dxTextBox | | ✓ | |
| `ApprovalUser` | Onaylayan | dxTextBox | | ✓ | |
| `ApprovalDate` | Onay Tarihi | dxDateBox | | ✓ | |
| `ApprovalDescription` | Onay Açıklaması | dxTextBox | | ✓ | |
### İzin Bakiyeleri — `App.Hr.LeaveBalance`
| Özellik | Değer |
| --- | --- |
| Wizard | `wizard/LeaveBalance.json` |
| Yol | Liste (Wizard) |
| Menü | `App.Hr.Leave` › `App.Hr.LeaveBalance` (sıra 2) |
| Yetki | `App.Hr.LeaveBalance` + `.Create`, `.Update`, `.Delete`, `.Export`, `.Import`, `.Note` |
| Açıklama | İzin Bakiyeleri — her çalışanın yıllık izin bakiyesi. |
| Kaynak | `Hr_V_LeaveSummary` |
| Görünümler | Grid, Card, Pivot, Chart (açılış grid) |
| Düzenleme | ekle ✗ · düzenle ✗ · sil ✗ · popup |
| Kapsam | Kiracı |
| Alt formlar | `App.Hr.LeaveBalanceAdjustment` (Id → EmployeeId), `App.Hr.LeaveBalanceYear` (Id → EmployeeId) |
| Widget'lar | `App.Widget.Total`, `App.Hr.Leave.Widget.RemainingDays`, `App.Hr.Leave.Widget.NegativeBalance`, `App.Hr.Leave.Widget.PendingDays` |
| Alan | Başlık | Editör | Zorunlu | Formda | Kaynak |
| --- | --- | --- | --- | --- | --- |
| `EmployeeName` | Çalışan Adı | dxTextBox | | ✓ | |
| `DepartmentId` | Departman | dxSelectBox | | ✓ | sorgu: Hr_T_Department |
| `JobPositionId` | İş Pozisyonu | dxSelectBox | | ✓ | sorgu: Hr_T_JobPosition |
| `SeniorityStartDate` | Kıdem Başlangıcı | dxDateBox | | ✓ | |
| `ServiceYears` | Kıdem Yılı | dxNumberBox | | ✓ | |
| `EntitledDays` | Hak Edilen Gün | dxNumberBox | | ✓ | |
| `AdjustmentDays` | Düzeltme Günü | dxNumberBox | | ✓ | |
| `UsedDays` | Kullanılan Gün | dxNumberBox | | ✓ | |
| `PendingDays` | Onay Bekleyen Gün | dxNumberBox | | ✓ | |
| `RemainingDays` | Kalan Gün | dxNumberBox | | ✓ | |
| `AvailableDays` | Kullanılabilir Gün | dxNumberBox | | ✓ | |
| `NextAccrualDate` | Sonraki Hak Ediş | dxDateBox | | ✓ | |
| `NextAccrualDays` | Sonraki Hak Ediş Günü | dxNumberBox | | ✓ | |
| `IsActive` | Aktif | dxCheckBox | | ✓ | |
### Bakiye Düzeltmeleri — `App.Hr.LeaveBalanceAdjustment`
| Özellik | Değer |
| --- | --- |
| Wizard | `wizard/LeaveBalanceAdjustment.json` |
| Yol | Liste (Wizard) |
| Menü | Menüsüz (alt form) |
| Yetki | `App.Hr.LeaveBalanceAdjustment` + `.Create`, `.Update`, `.Delete`, `.Export`, `.Import`, `.Note` |
| Açıklama | Bakiye Düzeltmeleri — çalışanın yıllık izin bakiyesine elle yapılan ekleme ve düşümler. |
| Kaynak | `Hr_T_LeaveBalanceAdjustment` · filtre `"IsDeleted" = 'false'` |
| Görünümler | Grid, Card, Pivot, Chart (açılış grid) |
| Düzenleme | ekle ✓ · düzenle ✓ · sil ✓ · popup |
| Kapsam | Kiracı |
| Form varsayılanları | IsActive=true, AdjustmentDate=@DATE |
| Alan | Başlık | Editör | Zorunlu | Formda | Kaynak |
| --- | --- | --- | --- | --- | --- |
| `EmployeeId` | Çalışan | dxSelectBox | | ✓ | sorgu: AbpUsers |
| `AdjustmentDate` | Düzeltme Tarihi | dxDateBox | ✓ | ✓ | |
| `Days` | Gün | dxNumberBox | ✓ | ✓ | |
| `IsActive` | Aktif | dxCheckBox | | ✓ | |
| `Reason` | Neden | dxTextArea | ✓ | ✓ | |
### İzin Defteri — `App.Hr.LeaveBalanceYear`
| Özellik | Değer |
| --- | --- |
| Wizard | `wizard/LeaveBalanceYear.json` |
| Yol | Liste (Wizard) |
| Menü | Menüsüz (alt form) |
| Yetki | `App.Hr.LeaveBalanceYear` + `.Create`, `.Update`, `.Delete`, `.Export`, `.Import`, `.Note` |
| Açıklama | İzin Defteri — çalışanın yıllık izninin yıl yıl dökümü. |
| Kaynak | `Hr_V_LeaveBalanceYear` |
| Görünümler | Grid, Card, Pivot, Chart (açılış grid) |
| Düzenleme | ekle ✗ · düzenle ✗ · sil ✗ · popup |
| Kapsam | Kiracı |
| Alan | Başlık | Editör | Zorunlu | Formda | Kaynak |
| --- | --- | --- | --- | --- | --- |
| `EmployeeId` | Çalışan | dxSelectBox | | ✓ | sorgu: AbpUsers |
| `YearNo` | Hizmet Yılı | dxNumberBox | | ✓ | |
| `AccrualDate` | Hak Ediş Tarihi | dxDateBox | | ✓ | |
| `PeriodEndDate` | Dönem Sonu | dxDateBox | | ✓ | |
| `EntitledDays` | Hak Edilen Gün | dxNumberBox | | ✓ | |
| `UsedDays` | Kullanılan Gün | dxNumberBox | | ✓ | |
| `PendingDays` | Onay Bekleyen Gün | dxNumberBox | | ✓ | |
| `RemainingDays` | Kalan Gün | dxNumberBox | | ✓ | |
| `CumulativeRemainingDays` | Kümülatif Bakiye | dxNumberBox | | ✓ | |
| `NextAccrualDate` | Sonraki Hak Ediş | dxDateBox | | ✓ | |
| `NextAccrualDays` | Sonraki Hak Ediş Günü | dxNumberBox | | ✓ | |
### İzin Taleplerim — `App.Hr.MyLeaveRequest`
| Özellik | Değer |
| --- | --- |
| Wizard | `wizard/MyLeaveRequest.json` |
| Yol | Liste (Wizard) |
| Menü | Menüsüz (alt form) |
| Yetki | `App.Hr.MyLeaveRequest` + `.Create`, `.Update`, `.Delete`, `.Export`, `.Import`, `.Note` |
| Açıklama | İzin Taleplerim — oturum açan çalışanın kendi izin talepleri ve onay durumları. |
| Kaynak | `Hr_T_LeaveRequest` · filtre `"IsDeleted" = 'false' AND "EmployeeId" = '@USERID'` |
| Görünümler | Grid, Card, Pivot, Chart (açılış grid) |
| Düzenleme | ekle ✓ · düzenle ✓ · sil ✓ · popup |
| Kapsam | Kiracı |
| Komutlar | InsertAfter: `Hr_S_LeaveRequestAfterSave` · UpdateBefore: `Hr_S_LeaveRequestGuard` · UpdateAfter: `Hr_S_LeaveRequestAfterSave` · DeleteBefore: `Hr_S_LeaveRequestGuard` · DeleteAfter: SQL |
| Form varsayılanları | IsHalfDay=false, StartDate=@DATE, EndDate=@DATE |
| Alan | Başlık | Editör | Zorunlu | Formda | Kaynak |
| --- | --- | --- | --- | --- | --- |
| `RequestNo` | Talep No | dxTextBox | | ✓ | |
| `LeaveTypeId` | İzin Türü | dxSelectBox | ✓ | ✓ | sorgu: Hr_T_LeaveType |
| `StartDate` | Başlangıç Tarihi | dxDateBox | ✓ | ✓ | script |
| `EndDate` | Bitiş Tarihi | dxDateBox | ✓ | ✓ | script |
| `IsHalfDay` | Yarım Gün | dxCheckBox | | ✓ | script |
| `HalfDayPart` | Yarım Gün Dilimi | dxSelectBox | | ✓ | liste: Morning, Afternoon |
| `Days` | Gün | dxNumberBox | | ✓ | |
| `BalanceWarning` | Bakiye Uyarısı | dxTextBox | | ✓ | |
| `Description` | Açıklama | dxTextArea | | ✓ | |
| `DocumentFileName` | Belge Adı | dxTextBox | | ✓ | |
| `DocumentContent` | Belge (rapor vb.) | dxImageUpload | | ✓ | |
| `ApprovalStatus` | Onay Durumu | dxTextBox | | ✓ | |
| `ApprovalUser` | Onaylayan | dxTextBox | | ✓ | |
| `ApprovalDate` | Onay Tarihi | dxDateBox | | ✓ | |
| `ApprovalDescription` | Onay Açıklaması | dxTextBox | | ✓ | |
### Ekibimin İzin Talepleri — `App.Hr.TeamLeaveRequest`
| Özellik | Değer |
| --- | --- |
| Wizard | `wizard/TeamLeaveRequest.json` |
| Yol | Liste (Wizard) |
| Menü | Menüsüz (alt form) |
| Yetki | `App.Hr.TeamLeaveRequest` + `.Create`, `.Update`, `.Delete`, `.Export`, `.Import`, `.Note` |
| Açıklama | Ekibimin İzin Talepleri — müdürü olduğunuz departmandaki çalışanların izin talepleri. |
| Kaynak | `Hr_T_LeaveRequest` · filtre `"IsDeleted" = 'false' AND '\|' + "ManagerIds" + '\|' LIKE '%\|@USERID\|%'` |
| Görünümler | Grid, Card, Pivot, Chart (açılış grid) |
| Düzenleme | ekle ✗ · düzenle ✗ · sil ✗ · popup |
| Kapsam | Kiracı |
| Komutlar | UpdateBefore: `Hr_S_LeaveRequestGuard` · UpdateAfter: `Hr_S_LeaveRequestAfterSave` |
| Onay akışı | Start 'Onaya Gönderildi' → Approval 'Müdür Onayı' → Approval 'İK Onayı' → End 'Onaylandı' → End 'Reddedildi' |
| Alan | Başlık | Editör | Zorunlu | Formda | Kaynak |
| --- | --- | --- | --- | --- | --- |
| `RequestNo` | Talep No | dxTextBox | | ✓ | |
| `EmployeeId` | Çalışan | dxSelectBox | | ✓ | sorgu: AbpUsers |
| `DepartmentId` | Departman | dxSelectBox | | ✓ | sorgu: Hr_T_Department |
| `JobPositionId` | İş Pozisyonu | dxSelectBox | | ✓ | sorgu: Hr_T_JobPosition |
| `LeaveTypeId` | İzin Türü | dxSelectBox | | ✓ | sorgu: Hr_T_LeaveType |
| `StartDate` | Başlangıç Tarihi | dxDateBox | | ✓ | |
| `EndDate` | Bitiş Tarihi | dxDateBox | | ✓ | |
| `IsHalfDay` | Yarım Gün | dxCheckBox | | ✓ | |
| `HalfDayPart` | Yarım Gün Dilimi | dxSelectBox | | ✓ | liste: Morning, Afternoon |
| `Days` | Gün | dxNumberBox | | ✓ | |
| `BalanceWarning` | Bakiye Uyarısı | dxTextBox | | ✓ | |
| `ManagerUserNames` | Departman Müdürleri | dxTextBox | | | |
| `Description` | Açıklama | dxTextArea | | ✓ | |
| `DocumentFileName` | Belge Adı | dxTextBox | | ✓ | |
| `DocumentContent` | Belge (rapor vb.) | dxImageUpload | | ✓ | |
| `ApprovalStatus` | Onay Durumu | dxTextBox | | ✓ | |
| `ApprovalUser` | Onaylayan | dxTextBox | | ✓ | |
| `ApprovalDate` | Onay Tarihi | dxDateBox | | ✓ | |
| `ApprovalDescription` | Onay Açıklaması | dxTextArea | | ✓ | |
### Bakiyem — `App.Hr.MyLeaveBalance`
| Özellik | Değer |
| --- | --- |
| Wizard | `wizard/MyLeaveBalance.json` |
| Yol | Liste (Wizard) |
| Menü | Menüsüz (alt form) |
| Yetki | `App.Hr.MyLeaveBalance` + `.Create`, `.Update`, `.Delete`, `.Export`, `.Import`, `.Note` |
| Açıklama | Bakiyem — yıllık izin hakkınızın ve kullanımınızın yıl yıl dökümü. |
| Kaynak | `Hr_V_LeaveBalanceYear` · filtre `"EmployeeId" = '@USERID'` |
| Görünümler | Grid, Card, Pivot, Chart (açılış grid) |
| Düzenleme | ekle ✗ · düzenle ✗ · sil ✗ · popup |
| Kapsam | Kiracı |
| Alan | Başlık | Editör | Zorunlu | Formda | Kaynak |
| --- | --- | --- | --- | --- | --- |
| `EmployeeId` | Çalışan | dxSelectBox | | ✓ | sorgu: AbpUsers |
| `YearNo` | Hizmet Yılı | dxNumberBox | | ✓ | |
| `AccrualDate` | Hak Ediş Tarihi | dxDateBox | | ✓ | |
| `PeriodEndDate` | Dönem Sonu | dxDateBox | | ✓ | |
| `EntitledDays` | Hak Edilen Gün | dxNumberBox | | ✓ | |
| `UsedDays` | Kullanılan Gün | dxNumberBox | | ✓ | |
| `PendingDays` | Onay Bekleyen Gün | dxNumberBox | | ✓ | |
| `RemainingDays` | Kalan Gün | dxNumberBox | | ✓ | |
| `CumulativeRemainingDays` | Kümülatif Bakiye | dxNumberBox | | ✓ | |
| `NextAccrualDate` | Sonraki Hak Ediş | dxDateBox | | ✓ | |
| `NextAccrualDays` | Sonraki Hak Ediş Günü | dxNumberBox | | ✓ | |
<!-- module-doc:auto:end -->

View file

@ -423,7 +423,7 @@ geri alınmaz.
| `CompetencyWeight` | Yetkinlik Ağırlığı % | dxNumberBox | ✓ | ✓ | script |
| `Is360Enabled` | 360 Derece Açık | dxCheckBox | | ✓ | |
| `IsActive` | Aktif | dxCheckBox | | ✓ | |
| `Description` | Açıklama | dxTextArea | | ✓ | |
| `Description` | Açıklama | dxTextBox | | ✓ | |
### Tüm Değerlendirmeler — `App.Hr.PerformanceReview`

View file

@ -0,0 +1,19 @@
CREATE OR ALTER FUNCTION [dbo].[Hr_F_HrSetting]
(
@TenantId uniqueidentifier,
@Name nvarchar(128)
)
RETURNS nvarchar(2048)
AS
BEGIN
-- Ayar ekranının çözüm sırası (T|G|D): kiracı değeri, genel değer, tanımdaki varsayılan.
RETURN COALESCE(
(SELECT TOP 1 [Value] FROM AbpSettings
WHERE [Name] = @Name AND ProviderName = N'T' AND @TenantId IS NOT NULL
AND ProviderKey = CONVERT(nvarchar(64), @TenantId)),
(SELECT TOP 1 [Value] FROM AbpSettings
WHERE [Name] = @Name AND ProviderName = N'G'),
(SELECT TOP 1 DefaultValue FROM Sas_H_SettingDefinition
WHERE Code = @Name AND IsDeleted = 0));
END
GO

View file

@ -0,0 +1,66 @@
CREATE OR ALTER FUNCTION [dbo].[Hr_F_LeaveWorkDays]
(
@TenantId uniqueidentifier,
@StartDate date,
@EndDate date
)
RETURNS decimal(9, 1)
AS
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 @StartDate IS NULL OR @EndDate IS NULL OR @EndDate < @StartDate
RETURN 0;
-- Gün sırası Pazartesi = 0 (DATEFIRST ayarından bağımsız). Ayar tanımı yoksa Pzt–Cum.
DECLARE @Work TABLE (DayIndex int PRIMARY KEY, IsWork bit);
INSERT INTO @Work (DayIndex, IsWork)
SELECT d.DayIndex,
CASE
WHEN s.Value IS NULL THEN CASE WHEN d.DayIndex < 5 THEN 1 ELSE 0 END
WHEN LOWER(s.Value) = N'true' THEN 1
ELSE 0
END
FROM (VALUES
(0, N'App.HrSettings.Leave.WorkMonday'),
(1, N'App.HrSettings.Leave.WorkTuesday'),
(2, N'App.HrSettings.Leave.WorkWednesday'),
(3, N'App.HrSettings.Leave.WorkThursday'),
(4, N'App.HrSettings.Leave.WorkFriday'),
(5, N'App.HrSettings.Leave.WorkSaturday'),
(6, N'App.HrSettings.Leave.WorkSunday')) d (DayIndex, SettingName)
CROSS APPLY (SELECT dbo.Hr_F_HrSetting(@TenantId, d.SettingName) AS Value) s;
DECLARE @Total decimal(9, 1) = 0;
DECLARE @Day date = @StartDate;
DECLARE @Value decimal(3, 1);
WHILE @Day <= @EndDate
BEGIN
SET @Value = 0;
IF EXISTS (SELECT 1 FROM @Work WHERE DayIndex = DATEDIFF(day, '19000101', @Day) % 7 AND IsWork = 1)
BEGIN
SET @Value = 1;
SELECT @Value = CASE WHEN MAX(CASE WHEN c.DayType = N'Holiday' THEN 1 ELSE 0 END) = 1 THEN 0 ELSE 0.5 END
FROM Hr_T_WorkCalendar c
WHERE c.IsDeleted = 0 AND c.IsActive = 1
AND (c.TenantId = @TenantId OR (c.TenantId IS NULL AND @TenantId IS NULL))
AND (
((c.RecurrenceRule IS NULL OR c.RecurrenceRule = N'') AND @Day BETWEEN c.StartDate AND c.EndDate)
OR (c.RecurrenceRule = N'FREQ=YEARLY' AND YEAR(@Day) >= YEAR(c.StartDate)
AND @Day BETWEEN DATEADD(year, YEAR(@Day) - YEAR(c.StartDate), c.StartDate)
AND DATEADD(year, YEAR(@Day) - YEAR(c.StartDate), c.EndDate))
)
HAVING COUNT(*) > 0;
END
SET @Total += @Value;
SET @Day = DATEADD(day, 1, @Day);
END
RETURN @Total;
END
GO

View file

@ -0,0 +1,42 @@
CREATE OR ALTER PROCEDURE [dbo].[Hr_S_LeaveNotify]
@RequestId uniqueidentifier,
@RecipientId uniqueidentifier,
@TypeName nvarchar(64),
@SubjectKey nvarchar(128),
@MessageKey nvarchar(128),
@ActorId uniqueidentifier = NULL
AS
BEGIN
SET NOCOUNT ON;
-- 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 @RequestId IS NULL OR @RecipientId IS NULL
RETURN;
INSERT INTO Adm_T_Notification (Id, TenantId, NotificationRuleId, NotificationChannel, NotificationTypeId, Identifier, UserId, Message, Subject, IsSent, IsRead, CreationTime, CreatorId, IsDeleted)
SELECT NEWID(), lr.TenantId, r.Id, r.Channel, t.Id,
CASE WHEN r.Channel = N'Mail' THEN u.Email ELSE CONVERT(nvarchar(64), u.Id) END,
u.Id,
LEFT(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(msg.Value, @MessageKey),
N'[Employee]', COALESCE(lr.EmployeeName, N'')),
N'[LeaveType]', COALESCE(lt.Name, N'')),
N'[StartDate]', FORMAT(lr.StartDate, 'dd.MM.yyyy')),
N'[EndDate]', FORMAT(lr.EndDate, 'dd.MM.yyyy')),
N'[Days]', FORMAT(lr.Days, '0.#')),
N'[Status]', COALESCE(lr.ApprovalStatus, N'')), 2048),
LEFT(COALESCE(sub.Value, @SubjectKey), 256),
CASE WHEN r.Channel = N'UiActivity' THEN 1 ELSE 0 END, 0, GETUTCDATE(), @ActorId, 0
FROM Hr_T_LeaveRequest lr
INNER JOIN Hr_T_LeaveType lt ON lt.Id = lr.LeaveTypeId
INNER JOIN AbpUsers u ON u.Id = @RecipientId AND u.IsActive = 1
INNER JOIN Adm_T_NotificationType t ON t.Name = @TypeName AND t.IsDeleted = 0
AND (t.TenantId = lr.TenantId OR (t.TenantId IS NULL AND lr.TenantId IS NULL))
INNER JOIN Adm_T_NotificationRule r ON r.NotificationTypeId = t.Id AND r.IsActive = 1 AND r.IsCustomized = 0
AND r.IsDeleted = 0 AND r.RecipientType = N'Custom' AND r.RecipientId IS NULL
LEFT JOIN Sas_H_LanguageText msg ON msg.ResourceName = N'Platform' AND msg.[Key] = @MessageKey AND msg.CultureName = N'tr'
LEFT JOIN Sas_H_LanguageText sub ON sub.ResourceName = N'Platform' AND sub.[Key] = @SubjectKey AND sub.CultureName = N'tr'
WHERE lr.Id = @RequestId
AND (r.Channel <> N'Mail' OR u.Email IS NOT NULL);
END
GO

View file

@ -0,0 +1,327 @@
CREATE OR ALTER PROCEDURE [dbo].[Hr_S_LeaveRequestAfterSave]
@RequestId uniqueidentifier,
@ActorId uniqueidentifier,
@Actor nvarchar(16)
AS
BEGIN
SET NOCOUNT ON;
-- İzin talebinin her kaydından (ekleme, güncelleme, Grid Workflow kararı) sonra çalışır:
-- çalışan bilgisi, talep no, müdür, gün sayısı tamamlanır; kurallar doğrulanır (THROW kaydı
-- geri alır); rapor türü Sağlık Kartı istirahatiyle eşlenir; durum değiştiyse bildirim gider.
DECLARE @StatusStart nvarchar(50) = N'Onaya Gönderildi',
@StatusManager nvarchar(50) = N'Müdür Onayı',
@StatusHr nvarchar(50) = N'İK Onayı',
@StatusApproved nvarchar(50) = N'Onaylandı',
@StatusRejected nvarchar(50) = N'Reddedildi';
DECLARE @TenantId uniqueidentifier, @EmployeeId uniqueidentifier, @LeaveTypeId uniqueidentifier,
@StartDate datetime2, @EndDate datetime2, @IsHalfDay bit, @HalfDayPart nvarchar(16),
@Document nvarchar(MAX), @SickLeaveId uniqueidentifier, @Status nvarchar(50),
@LastStatus nvarchar(50), @Description nvarchar(1024);
SELECT @TenantId = TenantId, @EmployeeId = EmployeeId, @LeaveTypeId = LeaveTypeId,
@StartDate = StartDate, @EndDate = EndDate, @IsHalfDay = IsHalfDay, @HalfDayPart = HalfDayPart,
@Document = DocumentContent, @SickLeaveId = SickLeaveId, @Status = ApprovalStatus,
@LastStatus = LastNotifiedStatus, @Description = [Description]
FROM Hr_T_LeaveRequest
WHERE Id = @RequestId AND IsDeleted = 0;
IF @EmployeeId IS NULL
RETURN;
DECLARE @Message nvarchar(1024);
---------------------------------------------------------------------------------------------
-- Çalışan ve departman müdürü
---------------------------------------------------------------------------------------------
DECLARE @EmployeeName nvarchar(256), @DepartmentId uniqueidentifier, @JobPositionId uniqueidentifier,
@Gender nvarchar(16);
SELECT @EmployeeName = CONCAT(Name, N' ', Surname), @DepartmentId = DepartmentId,
@JobPositionId = JobPositionId, @Gender = Gender
FROM AbpUsers
WHERE Id = @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.
DECLARE @Positions TABLE (Id uniqueidentifier PRIMARY KEY);
DECLARE @Managers TABLE (Id uniqueidentifier PRIMARY KEY, UserName nvarchar(256));
DECLARE @Parents TABLE (Id uniqueidentifier PRIMARY KEY);
DECLARE @Level int = 0;
INSERT INTO @Positions (Id)
SELECT p.Id
FROM Hr_T_JobPosition p
LEFT JOIN Hr_T_JobPosition parent ON parent.Id = p.ParentId
WHERE p.IsDeleted = 0 AND p.DepartmentId = @DepartmentId
AND (p.TenantId = @TenantId OR (p.TenantId IS NULL AND @TenantId IS NULL))
AND (p.ParentId IS NULL OR parent.DepartmentId <> p.DepartmentId);
IF EXISTS (SELECT 1 FROM @Positions WHERE Id = @JobPositionId)
BEGIN
DELETE FROM @Positions;
INSERT INTO @Positions (Id)
SELECT ParentId FROM Hr_T_JobPosition WHERE Id = @JobPositionId AND ParentId IS NOT NULL;
END
WHILE @Level < 10 AND EXISTS (SELECT 1 FROM @Positions)
BEGIN
INSERT INTO @Managers (Id, UserName)
SELECT u.Id, u.UserName
FROM AbpUsers u
WHERE u.IsDeleted = 0 AND u.IsActive = 1 AND u.Id <> @EmployeeId
AND (u.TenantId = @TenantId OR (u.TenantId IS NULL AND @TenantId IS NULL))
AND u.JobPositionId IN (SELECT Id FROM @Positions);
IF EXISTS (SELECT 1 FROM @Managers)
BREAK;
DELETE FROM @Parents;
INSERT INTO @Parents (Id)
SELECT DISTINCT p.ParentId FROM Hr_T_JobPosition p
WHERE p.Id IN (SELECT Id FROM @Positions) AND p.ParentId IS NOT NULL;
DELETE FROM @Positions;
INSERT INTO @Positions (Id) SELECT Id FROM @Parents;
SET @Level += 1;
END
DECLARE @ManagerIds nvarchar(1024) = (SELECT STRING_AGG(CONVERT(nvarchar(36), Id), N'|') FROM @Managers);
DECLARE @ManagerUserNames nvarchar(1024) = (SELECT STRING_AGG(UserName, N'|') FROM @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.
DECLARE @RequiresManager int =
CASE WHEN @ManagerIds IS NULL AND EXISTS (SELECT 1 FROM Hr_T_JobPosition
WHERE Id = @JobPositionId AND ParentId IS NULL AND IsDeleted = 0)
THEN 0 ELSE 1 END;
IF @ManagerIds IS NULL AND @RequiresManager = 1 AND (@Status IS NULL OR @Status IN (@StatusStart, @StatusManager))
THROW 51110, N'Ç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.', 1;
---------------------------------------------------------------------------------------------
-- 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 @Actor = N'Manager' AND COALESCE(@Status, N'') <> COALESCE(@LastStatus, N'')
AND NOT (@LastStatus = @StatusManager AND @Status IN (@StatusHr, @StatusRejected))
BEGIN
SET @Status = @LastStatus;
UPDATE Hr_T_LeaveRequest SET ApprovalStatus = @LastStatus WHERE Id = @RequestId;
END
-- Ç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 @Status IS NULL AND @Actor = N'Employee'
SET @Status = CASE WHEN @RequiresManager = 1 THEN @StatusManager ELSE @StatusHr END;
-- 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.
DECLARE @HrApprover nvarchar(1024) =
(SELECT TOP 1 Approver FROM Sas_H_ListFormWorkflow
WHERE ListFormCode = N'App.Hr.LeaveRequest' AND Kind = N'Approval' AND Title = @StatusHr);
UPDATE Hr_T_LeaveRequest
SET EmployeeName = @EmployeeName,
DepartmentId = @DepartmentId,
JobPositionId = @JobPositionId,
ManagerIds = COALESCE(@ManagerIds, ManagerIds),
ManagerUserNames = COALESCE(@ManagerUserNames, ManagerUserNames),
RequiresManagerApproval = @RequiresManager,
ApprovalStatus = @Status,
ApprovalUser = CASE
WHEN @Status = COALESCE(@LastStatus, N'') THEN ApprovalUser
WHEN @Status = @StatusManager THEN COALESCE(@ManagerUserNames, ApprovalUser)
WHEN @Status = @StatusHr AND @HrApprover IS NOT NULL THEN @HrApprover
ELSE ApprovalUser
END,
HalfDayPart = CASE WHEN @IsHalfDay = 1 THEN COALESCE(@HalfDayPart, N'Morning') ELSE NULL END,
RequestNo = CASE
WHEN RequestNo IS NULL OR RequestNo = N'' THEN
N'IZN-' + CAST(YEAR(CreationTime) AS nvarchar(4)) + N'-' + RIGHT(N'00000' + CAST(
(SELECT COALESCE(MAX(TRY_CAST(RIGHT(x.RequestNo, 5) AS int)), 0) + 1
FROM Hr_T_LeaveRequest x
WHERE (x.TenantId = @TenantId OR (x.TenantId IS NULL AND @TenantId IS NULL))
AND x.RequestNo LIKE N'IZN-' + CAST(YEAR(CreationTime) AS nvarchar(4)) + N'-%') AS nvarchar(10)), 5)
ELSE RequestNo
END
WHERE Id = @RequestId;
---------------------------------------------------------------------------------------------
-- Gün sayısı ve kurallar (sonuçlanmış talepte yeniden doğrulanmaz)
---------------------------------------------------------------------------------------------
DECLARE @TypeCode nvarchar(32), @TypeGender nvarchar(16), @MaxDaysPerYear int, @Deducts bit,
@RequiresDocument bit, @AllowHalfDay bit, @ExceedAction nvarchar(16);
SELECT @TypeCode = Code, @TypeGender = Gender, @MaxDaysPerYear = MaxDaysPerYear,
@Deducts = DeductsFromAnnualLeave, @RequiresDocument = RequiresDocument,
@AllowHalfDay = AllowHalfDay
FROM Hr_T_LeaveType
WHERE Id = @LeaveTypeId;
-- Kolon Hr_T_LeaveType.sql'de sonradan eklenir; migrate prosedürleri tablolardan önce
-- derlediği için kurulu bir veritabanında doğrudan başvuru derlemeyi düşürürdü.
EXEC sp_executesql N'SELECT @Action = BalanceExceedAction FROM Hr_T_LeaveType WHERE Id = @Id',
N'@Action nvarchar(16) OUTPUT, @Id uniqueidentifier', @Action = @ExceedAction OUTPUT, @Id = @LeaveTypeId;
DECLARE @Days decimal(9, 1) = dbo.Hr_F_LeaveWorkDays(@TenantId, CAST(@StartDate AS date), CAST(@EndDate AS date));
IF @IsHalfDay = 1
SET @Days = CASE WHEN @Days > 0.5 THEN 0.5 ELSE @Days END;
UPDATE Hr_T_LeaveRequest SET Days = @Days WHERE Id = @RequestId;
IF @Status IS NULL OR @Status IN (@StatusStart, @StatusManager, @StatusHr)
BEGIN
IF @TypeGender IS NOT NULL AND @TypeGender <> N'All' AND COALESCE(@Gender, N'') <> @TypeGender
THROW 51111, N'Bu izin türü çalışanın cinsiyetine uygun değil.', 1;
IF @IsHalfDay = 1 AND @AllowHalfDay = 0
THROW 51112, N'Bu izin türü yarım gün kullanılamaz.', 1;
IF @IsHalfDay = 1 AND CAST(@StartDate AS date) <> CAST(@EndDate AS date)
THROW 51113, N'Yarım gün izinde başlangıç ve bitiş aynı gün olmalıdır.', 1;
IF @RequiresDocument = 1 AND (@Document IS NULL OR @Document = N'')
THROW 51114, N'Bu izin türü için belge (rapor vb.) eklenmelidir.', 1;
IF @Days <= 0
THROW 51115, N'Seçilen tarih aralığında çalışma günü yok.', 1;
-- Ç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 Hr_T_LeaveRequest o
WHERE o.EmployeeId = @EmployeeId AND o.Id <> @RequestId AND o.IsDeleted = 0
AND (o.ApprovalStatus IS NULL OR o.ApprovalStatus <> @StatusRejected)
AND CAST(o.StartDate AS date) <= CAST(@EndDate AS date)
AND CAST(o.EndDate AS date) >= CAST(@StartDate AS date)
AND NOT (o.IsHalfDay = 1 AND @IsHalfDay = 1 AND COALESCE(o.HalfDayPart, N'Morning') <> COALESCE(@HalfDayPart, N'Morning')))
OR EXISTS (
SELECT 1 FROM Adm_T_UserSickLeave s
WHERE s.UserId = @EmployeeId AND s.IsDeleted = 0
AND (@SickLeaveId IS NULL OR s.Id <> @SickLeaveId)
AND NOT EXISTS (SELECT 1 FROM Hr_T_LeaveRequest l WHERE l.SickLeaveId = s.Id AND l.IsDeleted = 0)
AND CAST(s.StartDate AS date) <= CAST(@EndDate AS date)
AND CAST(s.EndDate AS date) >= CAST(@StartDate AS date))
THROW 51116, N'Bu tarihlerle çakışan başka bir izin ya da rapor kaydı var.', 1;
DECLARE @Warning nvarchar(512) = NULL;
IF @Deducts = 1
BEGIN
DECLARE @Remaining decimal(9, 1) = COALESCE((SELECT RemainingDays FROM Hr_V_LeaveSummary WHERE Id = @EmployeeId), 0);
DECLARE @OtherPending decimal(9, 1) = COALESCE((
SELECT SUM(o.Days) FROM Hr_T_LeaveRequest o
INNER JOIN Hr_T_LeaveType ot ON ot.Id = o.LeaveTypeId
WHERE o.EmployeeId = @EmployeeId AND o.Id <> @RequestId AND o.IsDeleted = 0
AND ot.DeductsFromAnnualLeave = 1
AND (o.ApprovalStatus IS NULL OR o.ApprovalStatus IN (@StatusStart, @StatusManager, @StatusHr))), 0);
IF @Days > @Remaining - @OtherPending
SET @Warning = CONCAT(N'Yıllık izin bakiyesi yetersiz: kullanılabilir ', FORMAT(@Remaining - @OtherPending, '0.#'),
N' gün, talep ', FORMAT(@Days, '0.#'), N' gün.');
END
IF @Warning IS NULL AND @MaxDaysPerYear IS NOT NULL
BEGIN
DECLARE @UsedThisYear decimal(9, 1) = COALESCE((
SELECT SUM(o.Days) FROM Hr_T_LeaveRequest o
WHERE o.EmployeeId = @EmployeeId AND o.Id <> @RequestId AND o.IsDeleted = 0
AND o.LeaveTypeId = @LeaveTypeId AND YEAR(o.StartDate) = YEAR(@StartDate)
AND (o.ApprovalStatus IS NULL OR o.ApprovalStatus <> @StatusRejected)), 0);
IF @UsedThisYear + @Days > @MaxDaysPerYear
SET @Warning = CONCAT(N'Bu izin türünün yıllık sınırı ', @MaxDaysPerYear, N' gün; bu yıl kullanılan/bekleyen ',
FORMAT(@UsedThisYear, '0.#'), N' gün, talep ', FORMAT(@Days, '0.#'), N' gün.');
END
IF @Warning IS NOT NULL AND COALESCE(@ExceedAction, N'Block') = N'Block'
BEGIN
SET @Message = @Warning;
THROW 51117, @Message, 1;
END
UPDATE Hr_T_LeaveRequest SET BalanceWarning = @Warning WHERE Id = @RequestId;
END
---------------------------------------------------------------------------------------------
-- Rapor türü = Sağlık Kartı › İstirahatler satırı (tek kayıt)
---------------------------------------------------------------------------------------------
IF @TypeCode = N'Sick' AND COALESCE(@Status, N'') <> @StatusRejected
BEGIN
IF @SickLeaveId IS NULL OR NOT EXISTS (SELECT 1 FROM Adm_T_UserSickLeave WHERE Id = @SickLeaveId)
BEGIN
SET @SickLeaveId = NEWID();
INSERT INTO Adm_T_UserSickLeave (Id, TenantId, UserId, StartDate, EndDate, Days, SickLeaveType, Institution, Clinic, SickLeaveReason, CreationTime, CreatorId, IsDeleted)
VALUES (@SickLeaveId, @TenantId, @EmployeeId, CAST(@StartDate AS date), CAST(@EndDate AS date),
DATEDIFF(day, CAST(@StartDate AS date), CAST(@EndDate AS date)) + 1, N'Disease', NULL, NULL,
LEFT(@Description, 512), GETUTCDATE(), @ActorId, 0);
UPDATE Hr_T_LeaveRequest SET SickLeaveId = @SickLeaveId WHERE Id = @RequestId;
END
ELSE
BEGIN
UPDATE Adm_T_UserSickLeave
SET StartDate = CAST(@StartDate AS date),
EndDate = CAST(@EndDate AS date),
Days = DATEDIFF(day, CAST(@StartDate AS date), CAST(@EndDate AS date)) + 1,
IsDeleted = 0, DeletionTime = NULL, DeleterId = NULL,
LastModificationTime = GETUTCDATE(), LastModifierId = @ActorId
WHERE Id = @SickLeaveId;
END
END
ELSE IF @SickLeaveId IS NOT NULL
BEGIN
-- Reddedilen ya da türü rapordan başka bir türe çevrilen talebin istirahati kalkar.
UPDATE Adm_T_UserSickLeave
SET IsDeleted = 1, DeletionTime = GETUTCDATE(), DeleterId = @ActorId
WHERE Id = @SickLeaveId AND IsDeleted = 0;
IF @TypeCode <> N'Sick'
UPDATE Hr_T_LeaveRequest SET SickLeaveId = NULL WHERE Id = @RequestId;
END
---------------------------------------------------------------------------------------------
-- Bildirim: yalnızca bildirim taşıyan durumlara geçişte, bir kez
---------------------------------------------------------------------------------------------
IF @Status IN (@StatusManager, @StatusHr, @StatusApproved, @StatusRejected)
AND COALESCE(@LastStatus, N'') <> @Status
BEGIN
DECLARE @Recipients TABLE (Id uniqueidentifier PRIMARY KEY);
IF @Status = @StatusManager
INSERT INTO @Recipients (Id) SELECT Id FROM @Managers;
ELSE IF @Status = @StatusHr
INSERT INTO @Recipients (Id)
SELECT DISTINCT u.Id
FROM STRING_SPLIT(COALESCE(@HrApprover, N''), N'|') a
INNER JOIN AbpUsers u ON (u.UserName = LTRIM(RTRIM(a.value)) OR u.Email = LTRIM(RTRIM(a.value)))
AND u.IsDeleted = 0
AND (u.TenantId = @TenantId OR (u.TenantId IS NULL AND @TenantId IS NULL))
WHERE LTRIM(RTRIM(a.value)) <> N'';
ELSE
INSERT INTO @Recipients (Id) VALUES (@EmployeeId);
DECLARE @Type nvarchar(64) = CASE WHEN @Status IN (@StatusManager, @StatusHr) THEN N'IzinTalebiOnayBekliyor' ELSE N'IzinTalebiSonuclandi' END;
DECLARE @Subject nvarchar(128) = CASE WHEN @Status IN (@StatusManager, @StatusHr) THEN N'App.Hr.Leave.Notification.Approval.Subject' ELSE N'App.Hr.Leave.Notification.Result.Subject' END;
DECLARE @Body nvarchar(128) = CASE WHEN @Status IN (@StatusManager, @StatusHr) THEN N'App.Hr.Leave.Notification.Approval.Message' ELSE N'App.Hr.Leave.Notification.Result.Message' END;
DECLARE @Recipient uniqueidentifier;
DECLARE recipient_cursor CURSOR LOCAL FAST_FORWARD FOR SELECT Id FROM @Recipients;
OPEN recipient_cursor;
FETCH NEXT FROM recipient_cursor INTO @Recipient;
WHILE @@FETCH_STATUS = 0
BEGIN
EXEC dbo.Hr_S_LeaveNotify @RequestId, @Recipient, @Type, @Subject, @Body, @ActorId;
FETCH NEXT FROM recipient_cursor INTO @Recipient;
END
CLOSE recipient_cursor;
DEALLOCATE recipient_cursor;
UPDATE Hr_T_LeaveRequest SET LastNotifiedStatus = @Status WHERE Id = @RequestId;
END
END
GO

View file

@ -0,0 +1,48 @@
CREATE OR ALTER PROCEDURE [dbo].[Hr_S_LeaveRequestGuard]
@RequestId uniqueidentifier,
@ActorId uniqueidentifier,
@Actor nvarchar(16),
@Operation nvarchar(16)
AS
BEGIN
SET NOCOUNT ON;
-- 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.
IF @RequestId IS NULL OR @Actor = N'Hr'
RETURN;
DECLARE @EmployeeId uniqueidentifier, @Status nvarchar(50), @ManagerIds nvarchar(1024);
SELECT @EmployeeId = EmployeeId, @Status = ApprovalStatus, @ManagerIds = ManagerIds
FROM Hr_T_LeaveRequest
WHERE Id = @RequestId AND IsDeleted = 0;
IF @EmployeeId IS NULL
RETURN;
IF @Actor = N'Employee'
BEGIN
IF @EmployeeId <> @ActorId
THROW 51100, N'Bu izin talebi size ait değil.', 1;
-- Karar verilmiş talep çalışan tarafından değiştirilemez; iptal için İK'ya başvurulur.
IF @Status IS NOT NULL AND @Status NOT IN (N'Onaya Gönderildi', N'Müdür Onayı')
THROW 51101, N'Onay sürecinde ilerlemiş ya da sonuçlanmış izin talebi değiştirilemez veya silinemez. İptal için İnsan Kaynakları ile görüşün.', 1;
RETURN;
END
IF @Actor = N'Manager'
BEGIN
IF @Operation = N'Delete'
THROW 51102, N'Yönetici izin talebini silemez.', 1;
IF @ActorId IS NULL OR N'|' + COALESCE(@ManagerIds, N'') + N'|' NOT LIKE N'%|' + CONVERT(nvarchar(36), @ActorId) + N'|%'
THROW 51103, N'Bu izin talebinin onaycısı değilsiniz.', 1;
IF @Status IS NULL OR @Status <> N'Müdür Onayı'
THROW 51104, N'İzin talebi müdür onayında beklemiyor.', 1;
END
END
GO

View file

@ -0,0 +1,77 @@
CREATE OR ALTER PROCEDURE [dbo].[Hr_S_LeaveSickLeaveSync]
@SickLeaveId uniqueidentifier,
@ActorId uniqueidentifier = NULL
AS
BEGIN
SET NOCOUNT ON;
-- 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 @SickLeaveId IS NULL
RETURN;
DECLARE @TenantId uniqueidentifier, @UserId uniqueidentifier, @StartDate date, @EndDate date,
@Reason nvarchar(1024), @IsDeleted bit;
SELECT @TenantId = TenantId, @UserId = UserId, @StartDate = CAST(StartDate AS date),
@EndDate = CAST(EndDate AS date), @Reason = SickLeaveReason, @IsDeleted = IsDeleted
FROM Adm_T_UserSickLeave
WHERE Id = @SickLeaveId;
IF @UserId IS NULL
RETURN;
DECLARE @RequestId uniqueidentifier =
(SELECT TOP 1 Id FROM Hr_T_LeaveRequest WHERE SickLeaveId = @SickLeaveId ORDER BY IsDeleted, CreationTime);
IF @IsDeleted = 1
BEGIN
UPDATE Hr_T_LeaveRequest
SET IsDeleted = 1, DeletionTime = GETUTCDATE(), DeleterId = @ActorId
WHERE SickLeaveId = @SickLeaveId AND IsDeleted = 0;
RETURN;
END
DECLARE @Days decimal(9, 1) = dbo.Hr_F_LeaveWorkDays(@TenantId, @StartDate, @EndDate);
IF @RequestId IS NOT NULL
BEGIN
UPDATE Hr_T_LeaveRequest
SET StartDate = @StartDate, EndDate = @EndDate, Days = @Days,
IsDeleted = 0, DeletionTime = NULL, DeleterId = NULL,
LastModificationTime = GETUTCDATE(), LastModifierId = @ActorId
WHERE Id = @RequestId;
RETURN;
END
-- Kiracıda rapor türü tanımlı değilse eşlenecek talep yoktur.
DECLARE @LeaveTypeId uniqueidentifier =
(SELECT TOP 1 Id FROM Hr_T_LeaveType
WHERE Code = N'Sick' AND IsDeleted = 0
AND (TenantId = @TenantId OR (TenantId IS NULL AND @TenantId IS NULL)));
IF @LeaveTypeId IS NULL
RETURN;
DECLARE @ActorName nvarchar(256) = (SELECT UserName FROM AbpUsers WHERE Id = @ActorId);
DECLARE @Year nvarchar(4) = CAST(YEAR(GETUTCDATE()) AS nvarchar(4));
-- İstirahat sağlık birimince girilmiş bir belgedir; onay akışına girmeden onaylı yazılır.
INSERT INTO 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 NEWID(), @TenantId,
N'IZN-' + @Year + N'-' + RIGHT(N'00000' + CAST(
(SELECT COALESCE(MAX(TRY_CAST(RIGHT(x.RequestNo, 5) AS int)), 0) + 1
FROM Hr_T_LeaveRequest x
WHERE (x.TenantId = @TenantId OR (x.TenantId IS NULL AND @TenantId IS NULL))
AND x.RequestNo LIKE N'IZN-' + @Year + N'-%') AS nvarchar(10)), 5),
u.Id, CONCAT(u.Name, N' ', u.Surname), u.DepartmentId, u.JobPositionId, @LeaveTypeId,
@StartDate, @EndDate, 0, @Days, LEFT(@Reason, 1024), @SickLeaveId,
N'Onaylandı', @ActorName, GETUTCDATE(), N'Onaylandı', 1, GETUTCDATE(), @ActorId, 0
FROM AbpUsers u
WHERE u.Id = @UserId;
END
GO

View file

@ -0,0 +1,33 @@
IF OBJECT_ID(N'[dbo].[Hr_T_LeaveBalanceAdjustment]', 'U') IS NULL
BEGIN
CREATE TABLE [dbo].[Hr_T_LeaveBalanceAdjustment]
(
[Id] uniqueidentifier NOT NULL DEFAULT NEWID(),
[TenantId] uniqueidentifier NULL,
[EmployeeId] uniqueidentifier NOT NULL,
[AdjustmentDate] date NOT NULL,
[Days] decimal(5, 1) NOT NULL,
[Reason] nvarchar(512) NOT NULL,
[IsActive] bit NOT NULL DEFAULT 1,
[CreationTime] datetime2 NOT NULL DEFAULT GETUTCDATE(),
[CreatorId] uniqueidentifier NULL,
[LastModificationTime] datetime2 NULL,
[LastModifierId] uniqueidentifier NULL,
[IsDeleted] bit NOT NULL DEFAULT 0,
[DeletionTime] datetime2 NULL,
[DeleterId] uniqueidentifier NULL,
CONSTRAINT [PK_Hr_T_LeaveBalanceAdjustment] PRIMARY KEY NONCLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
END
GO
IF OBJECT_ID(N'[dbo].[CK_Hr_T_LeaveBalanceAdjustment_Days]', 'C') IS NULL
ALTER TABLE [dbo].[Hr_T_LeaveBalanceAdjustment] ADD CONSTRAINT [CK_Hr_T_LeaveBalanceAdjustment_Days] CHECK ([Days] <> 0)
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_Hr_T_LeaveBalanceAdjustment_Employee' AND object_id = OBJECT_ID(N'[dbo].[Hr_T_LeaveBalanceAdjustment]'))
CREATE NONCLUSTERED INDEX [IX_Hr_T_LeaveBalanceAdjustment_Employee] ON [dbo].[Hr_T_LeaveBalanceAdjustment] ([TenantId], [EmployeeId]) WHERE [IsDeleted] = 0
GO

View file

@ -0,0 +1,65 @@
IF OBJECT_ID(N'[dbo].[Hr_T_LeaveRequest]', 'U') IS NULL
BEGIN
CREATE TABLE [dbo].[Hr_T_LeaveRequest]
(
[Id] uniqueidentifier NOT NULL DEFAULT NEWID(),
[TenantId] uniqueidentifier NULL,
[RequestNo] nvarchar(32) NULL,
[EmployeeId] uniqueidentifier NOT NULL,
[EmployeeName] nvarchar(256) NULL,
[DepartmentId] uniqueidentifier NULL,
[JobPositionId] uniqueidentifier NULL,
[LeaveTypeId] uniqueidentifier NOT NULL,
[StartDate] datetime2 NOT NULL,
[EndDate] datetime2 NOT NULL,
[IsHalfDay] bit NOT NULL DEFAULT 0,
[HalfDayPart] nvarchar(16) NULL,
[Days] decimal(5, 1) NOT NULL DEFAULT 0,
[Description] nvarchar(1024) NULL,
[DocumentFileName] nvarchar(256) NULL,
[DocumentContent] nvarchar(MAX) NULL,
[BalanceWarning] nvarchar(512) NULL,
[SickLeaveId] uniqueidentifier NULL,
[ManagerIds] nvarchar(1024) NULL,
[ManagerUserNames] nvarchar(1024) NULL,
[RequiresManagerApproval] int NOT NULL DEFAULT 1,
[ApprovalStatus] nvarchar(50) NULL,
[ApprovalUser] nvarchar(1024) NULL,
[ApprovalDate] datetime2 NULL,
[ApprovalDescription] nvarchar(512) NULL,
[LastNotifiedStatus] nvarchar(50) NULL,
[IsActive] bit NOT NULL DEFAULT 1,
[CreationTime] datetime2 NOT NULL DEFAULT GETUTCDATE(),
[CreatorId] uniqueidentifier NULL,
[LastModificationTime] datetime2 NULL,
[LastModifierId] uniqueidentifier NULL,
[IsDeleted] bit NOT NULL DEFAULT 0,
[DeletionTime] datetime2 NULL,
[DeleterId] uniqueidentifier NULL,
CONSTRAINT [PK_Hr_T_LeaveRequest] PRIMARY KEY NONCLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
END
GO
IF COL_LENGTH(N'dbo.Hr_T_LeaveRequest', N'RequiresManagerApproval') IS NULL
ALTER TABLE [dbo].[Hr_T_LeaveRequest] ADD [RequiresManagerApproval] int NOT NULL DEFAULT 1
GO
IF OBJECT_ID(N'[dbo].[CK_Hr_T_LeaveRequest_Dates]', 'C') IS NULL
ALTER TABLE [dbo].[Hr_T_LeaveRequest] ADD CONSTRAINT [CK_Hr_T_LeaveRequest_Dates] CHECK ([EndDate] >= [StartDate])
GO
IF OBJECT_ID(N'[dbo].[CK_Hr_T_LeaveRequest_HalfDayPart]', 'C') IS NULL
ALTER TABLE [dbo].[Hr_T_LeaveRequest] ADD CONSTRAINT [CK_Hr_T_LeaveRequest_HalfDayPart] CHECK ([HalfDayPart] IS NULL OR [HalfDayPart] IN ('Morning', 'Afternoon'))
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_Hr_T_LeaveRequest_Employee' AND object_id = OBJECT_ID(N'[dbo].[Hr_T_LeaveRequest]'))
CREATE NONCLUSTERED INDEX [IX_Hr_T_LeaveRequest_Employee] ON [dbo].[Hr_T_LeaveRequest] ([TenantId], [EmployeeId], [StartDate]) WHERE [IsDeleted] = 0
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_Hr_T_LeaveRequest_SickLeave' AND object_id = OBJECT_ID(N'[dbo].[Hr_T_LeaveRequest]'))
CREATE NONCLUSTERED INDEX [IX_Hr_T_LeaveRequest_SickLeave] ON [dbo].[Hr_T_LeaveRequest] ([SickLeaveId]) WHERE [SickLeaveId] IS NOT NULL
GO

View file

@ -12,6 +12,7 @@ BEGIN
[DeductsFromAnnualLeave] bit NOT NULL DEFAULT 0,
[RequiresDocument] bit NOT NULL DEFAULT 0,
[AllowHalfDay] bit NOT NULL DEFAULT 0,
[BalanceExceedAction] nvarchar(16) NOT NULL DEFAULT 'Block',
[IsActive] bit NOT NULL DEFAULT 1,
[CreationTime] datetime2 NOT NULL DEFAULT GETUTCDATE(),
[CreatorId] uniqueidentifier NULL,
@ -26,4 +27,13 @@ BEGIN
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
END
GO
GO
-- Bakiye kuralı izin talebi ekranıyla geldi; daha önce kurulmuş tabloya kayıpsız eklenir.
IF COL_LENGTH(N'dbo.Hr_T_LeaveType', N'BalanceExceedAction') IS NULL
ALTER TABLE [dbo].[Hr_T_LeaveType] ADD [BalanceExceedAction] nvarchar(16) NOT NULL DEFAULT 'Block'
GO
IF OBJECT_ID(N'[dbo].[CK_Hr_T_LeaveType_BalanceExceedAction]', 'C') IS NULL
ALTER TABLE [dbo].[Hr_T_LeaveType] ADD CONSTRAINT [CK_Hr_T_LeaveType_BalanceExceedAction] CHECK ([BalanceExceedAction] IN ('Block', 'Warn'))
GO

View file

@ -0,0 +1,114 @@
CREATE OR ALTER VIEW [dbo].[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(TRY_CAST(dbo.Hr_F_HrSetting(t.TenantId, N'App.HrSettings.Leave.AnnualDays1To5') AS decimal(5, 1)), 14) AS Days1To5,
COALESCE(TRY_CAST(dbo.Hr_F_HrSetting(t.TenantId, N'App.HrSettings.Leave.AnnualDays5To15') AS decimal(5, 1)), 20) AS Days5To15,
COALESCE(TRY_CAST(dbo.Hr_F_HrSetting(t.TenantId, N'App.HrSettings.Leave.AnnualDays15Plus') AS decimal(5, 1)), 26) AS Days15Plus,
COALESCE(TRY_CAST(dbo.Hr_F_HrSetting(t.TenantId, N'App.HrSettings.Leave.MinDaysYoungOld') AS decimal(5, 1)), 20) AS MinDaysYoungOld
FROM (SELECT DISTINCT TenantId FROM AbpUsers WHERE IsDeleted = 0) t
),
Emp AS (
SELECT u.Id AS EmployeeId, u.TenantId,
CAST(COALESCE(u.SeniorityStartDate, u.HireDate) AS date) AS SeniorityStartDate,
CAST(u.BirthDate AS date) AS BirthDate,
CAST(u.TerminationDate AS date) AS TerminationDate,
c.Days1To5, c.Days5To15, c.Days15Plus, c.MinDaysYoungOld
FROM AbpUsers u
INNER JOIN TenantCfg c ON c.TenantId = u.TenantId OR (c.TenantId IS NULL AND u.TenantId IS NULL)
WHERE u.IsDeleted = 0
),
Years AS (
SELECT e.*, n.YearNo,
DATEADD(year, n.YearNo, e.SeniorityStartDate) AS AccrualDate,
DATEADD(year, n.YearNo + 1, e.SeniorityStartDate) AS NextPeriodDate
FROM Emp e
CROSS JOIN (VALUES (1), (2), (3), (4), (5), (6), (7), (8), (9), (10), (11), (12), (13), (14), (15),
(16), (17), (18), (19), (20), (21), (22), (23), (24), (25), (26), (27), (28), (29), (30),
(31), (32), (33), (34), (35), (36), (37), (38), (39), (40), (41), (42), (43), (44), (45),
(46), (47), (48), (49), (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 DATEDIFF(year, y.BirthDate, y.AccrualDate)
- CASE WHEN DATEADD(year, DATEDIFF(year, y.BirthDate, y.AccrualDate), y.BirthDate) > 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 <= CAST(GETDATE() AS 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, CAST(r.StartDate AS date) AS StartDate, r.Days,
CASE WHEN r.ApprovalStatus = N'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 = 0 AND t.DeductsFromAnnualLeave = 1
AND (r.ApprovalStatus IS NULL OR r.ApprovalStatus <> N'Reddedildi')
),
Ledger AS (
SELECT a.EmployeeId, a.TenantId, a.YearNo, a.AccrualDate, DATEADD(day, -1, a.NextPeriodDate) AS PeriodEndDate,
a.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) 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) AS PendingDays
FROM Accrued a
WHERE a.IsAccrued = 1
UNION ALL
SELECT e.EmployeeId, e.TenantId, 0, NULL, NULL,
COALESCE((SELECT SUM(x.Days) FROM Hr_T_LeaveBalanceAdjustment x
WHERE x.EmployeeId = e.EmployeeId AND x.IsDeleted = 0 AND x.IsActive = 1), 0),
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 < DATEADD(year, 1, e.SeniorityStartDate))), 0),
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 < DATEADD(year, 1, e.SeniorityStartDate))), 0)
FROM Emp e
)
SELECT CONCAT(CONVERT(nvarchar(36), l.EmployeeId), N'-', l.YearNo) AS Id,
l.TenantId,
l.EmployeeId,
CONCAT(u.Name, N' ', u.Surname) AS EmployeeName,
u.DepartmentId,
u.JobPositionId,
l.YearNo,
l.AccrualDate,
l.PeriodEndDate,
l.EntitledDays,
l.UsedDays,
l.PendingDays,
l.EntitledDays - l.UsedDays AS RemainingDays,
SUM(l.EntitledDays - l.UsedDays) OVER (PARTITION BY l.EmployeeId ORDER BY l.YearNo
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS CumulativeRemainingDays,
na.NextAccrualDate,
(SELECT a.EntitledDays FROM Accrued a WHERE a.EmployeeId = l.EmployeeId AND a.AccrualDate = na.NextAccrualDate) 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
GO

View file

@ -0,0 +1,32 @@
CREATE OR ALTER VIEW [dbo].[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, N' ', u.Surname) AS EmployeeName,
u.DepartmentId,
u.JobPositionId,
CAST(COALESCE(u.SeniorityStartDate, u.HireDate) AS date) AS SeniorityStartDate,
CASE
WHEN COALESCE(u.SeniorityStartDate, u.HireDate) IS NULL THEN NULL
ELSE DATEDIFF(year, COALESCE(u.SeniorityStartDate, u.HireDate), GETDATE())
- CASE WHEN DATEADD(year, DATEDIFF(year, COALESCE(u.SeniorityStartDate, u.HireDate), GETDATE()),
COALESCE(u.SeniorityStartDate, u.HireDate)) > GETDATE() THEN 1 ELSE 0 END
END AS ServiceYears,
CAST(COALESCE(SUM(CASE WHEN l.YearNo > 0 THEN l.EntitledDays END), 0) AS decimal(9, 1)) AS EntitledDays,
CAST(COALESCE(SUM(CASE WHEN l.YearNo = 0 THEN l.EntitledDays END), 0) AS decimal(9, 1)) AS AdjustmentDays,
CAST(COALESCE(SUM(l.UsedDays), 0) AS decimal(9, 1)) AS UsedDays,
CAST(COALESCE(SUM(l.PendingDays), 0) AS decimal(9, 1)) AS PendingDays,
CAST(COALESCE(SUM(l.EntitledDays - l.UsedDays), 0) AS decimal(9, 1)) AS RemainingDays,
CAST(COALESCE(SUM(l.EntitledDays - l.UsedDays - l.PendingDays), 0) AS decimal(9, 1)) AS AvailableDays,
MAX(l.NextAccrualDate) AS NextAccrualDate,
MAX(l.NextAccrualDays) AS NextAccrualDays,
u.IsActive
FROM AbpUsers u
LEFT JOIN Hr_V_LeaveBalanceYear l ON l.EmployeeId = u.Id
WHERE u.IsDeleted = 0
GROUP BY u.Id, u.TenantId, u.Name, u.Surname, u.DepartmentId, u.JobPositionId,
u.SeniorityStartDate, u.HireDate, u.IsActive
GO

View file

@ -25,8 +25,8 @@
"MenuTr": "İstirahat Raporları",
"TitleEn": "Sick Leaves",
"TitleTr": "İstirahat Raporları",
"DescEn": "Sick Leaves — the employee's sick leave records.\nWhere: the Sick Leaves tab of Health Cards.\nFields: start and end date, days (end inclusive, automatic), type (Disease, Work accident, Occupational disease, Maternity), institution, clinic, reason.\nNote: sick leave does not affect leave balances.",
"DescTr": "İstirahat Raporları — çalışanın aldığı istirahat (rapor) kayıtları.\nNerede: Sağlık Kartları ekranında kaydın İstirahat Raporları sekmesi.\nAlanlar: başlangıç ve bitiş tarihi, gün (bitiş dahil, otomatik), istirahat türü (Hastalık, İş kazası, Meslek hastalığı, Doğum), kurum, klinik, neden.\nDikkat: rapor izin bakiyesine yansımaz."
"DescEn": "Sick Leaves — the employee's sick leave records.\nWhere: the Sick Leaves tab of Health Cards.\nFields: start and end date, days (end inclusive, automatic), type (Disease, Work accident, Occupational disease, Maternity), institution, clinic, reason.\nSingle record: a report entered here appears in Human Resources › Leave Management as an approved leave request of type Sick, and a sick leave requested by the employee on the Intranet appears in this tab. Deleting the report removes the leave request too. Sick leave does not reduce the annual leave balance.",
"DescTr": "İstirahat Raporları — çalışanın aldığı istirahat (rapor) kayıtları.\nNerede: Sağlık Kartları ekranında kaydın İstirahat Raporları sekmesi.\nAlanlar: başlangıç ve bitiş tarihi, gün (bitiş dahil, otomatik), istirahat türü (Hastalık, İş kazası, Meslek hastalığı, Doğum), kurum, klinik, neden.\nTek kayıt: buradan girilen rapor İnsan Kaynakları › İzin Yönetimi'nde Rapor (Sick) türünde onaylı izin talebi olarak görünür; çalışanın Intranet'ten girdiği rapor talebi de bu sekmeye düşer. Silinen rapor izin talebinden de kalkar. Rapor yıllık izin bakiyesinden düşmez."
},
"Groups": [
{
@ -104,7 +104,7 @@
"SeedSyncDelete": false,
"DeleteCommand": "UPDATE \"Adm_T_UserSickLeave\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\" IN @Id",
"DeleteBeforeCommand": null,
"DeleteAfterCommand": null,
"DeleteAfterCommand": "UPDATE Hr_T_LeaveRequest SET IsDeleted = 1, DeletionTime = GETUTCDATE(), DeleterId = @DeleterId WHERE SickLeaveId IN @Id AND IsDeleted = 0",
"DeleteFieldsDefaultValueDto": [
{
"FieldName": "DeleterId",
@ -123,7 +123,7 @@
],
"InsertCommand": null,
"InsertBeforeCommand": null,
"InsertAfterCommand": null,
"InsertAfterCommand": "EXEC dbo.Hr_S_LeaveSickLeaveSync @Id, @CreatorId",
"InsertFieldsDefaultValueDto": [
{
"FieldName": "CreationTime",
@ -156,8 +156,23 @@
],
"UpdateCommand": null,
"UpdateBeforeCommand": null,
"UpdateAfterCommand": null,
"UpdateFieldsDefaultValueDto": [],
"UpdateAfterCommand": "DECLARE @_id uniqueidentifier = (SELECT TOP 1 Id FROM Adm_T_UserSickLeave WHERE Id IN @Id); EXEC dbo.Hr_S_LeaveSickLeaveSync @_id, @LastModifierId",
"UpdateFieldsDefaultValueDto": [
{
"FieldName": "LastModificationTime",
"FieldDbType": 6,
"Value": "@NOW",
"CustomValueType": 2,
"SqlQuery": ""
},
{
"FieldName": "LastModifierId",
"FieldDbType": 9,
"Value": "@USERID",
"CustomValueType": 2,
"SqlQuery": ""
}
],
"FormFieldsDefaultValueDto": [],
"WidgetsDto": [],
"ExtraFilterEditDto": [],

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -32,8 +32,8 @@
"MenuTr": "İzin Türleri",
"TitleEn": "Leave Types",
"TitleTr": "İzin Türleri",
"DescEn": "Leave Types — kinds of leave and their rules (annual, excuse, unpaid, sick, maternity, paternity, marriage, bereavement, nursing).\nWhere: Human Resources › Definitions › Leave Types.\nFields: code, name, gender (All/Male/Female), max days per year, paid, deducts from annual leave, document required, half-day allowed, active.\nNote: not yet linked to a leave request screen; it is the source definition for a future leave process.",
"DescTr": "İzin Türleri — izin çeşitleri ve kuralları (yıllık, mazeret, ücretsiz, hastalık, doğum, babalık, evlilik, ölüm, süt izni).\nNerede: İnsan Kaynakları › Tanımlamalar › İzin Türleri.\nAlanlar: kod, ad, cinsiyet (Tümü/Erkek/Kadın), yıllık en fazla gün, ücretli, yıllık izinden düşer, belge gerekir, yarım gün kullanılabilir, aktif.\nDikkat: henüz bir izin talep ekranına bağlı değildir; izin talep süreci kurulduğunda kaynak tanım olarak kullanılacaktır.",
"DescEn": "Leave Types — kinds of leave and their rules (annual, excuse, unpaid, sick, maternity, paternity, marriage, bereavement, nursing).\nWhere: Human Resources › Definitions › Leave Types. Used by Human Resources › Leave Management and the My Leaves widget on Intranet › Dashboard.\nFields: code, name, gender (All/Male/Female — the type is offered only to that gender), max days per year, paid, deducts from annual leave (the request uses the annual balance), document required (a document must be attached), half-day allowed, when balance is exceeded (Block / Warn and save), active.\nNote: the type with code Sick is the same record as Health Card › Sick Leaves.",
"DescTr": "İzin Türleri — izin çeşitleri ve kuralları (yıllık, mazeret, ücretsiz, hastalık, doğum, babalık, evlilik, ölüm, süt izni).\nNerede: İnsan Kaynakları › Tanımlamalar › İzin Türleri. İnsan Kaynakları › İzin Yönetimi ve Intranet › Dashboard'daki İzinlerim widget'ı kullanır.\nAlanlar: kod, ad, cinsiyet (Tümü/Erkek/Kadın — tür yalnız o cinsiyete sunulur), yıllık en fazla gün, ücretli, yıllık izinden düşer (talep yıllık bakiyeden düşülür), belge gerekir (talepte belge zorunlu), yarım gün kullanılabilir, bakiye aşılırsa (Talebi engelle / Uyar ve kaydet), aktif.\nDikkat: kodu Sick olan tür Sağlık Kartı › İstirahatler ile aynı kayıttır.",
"Texts": [
{
"Key": "App.Widget.Paid",
@ -44,6 +44,16 @@
"Key": "App.Widget.Unpaid",
"En": "Unpaid",
"Tr": "Ücretsiz"
},
{
"Key": "App.Hr.LeaveType.BalanceExceedAction.Block",
"En": "Block the request",
"Tr": "Talebi engelle"
},
{
"Key": "App.Hr.LeaveType.BalanceExceedAction.Warn",
"En": "Warn and save",
"Tr": "Uyar ve kaydet"
}
]
},
@ -104,6 +114,12 @@
"En": "Half Day Allowed",
"Tr": "Yarım Gün Kullanılabilir"
},
{
"FieldName": "BalanceExceedAction",
"CaptionName": "App.Listform.ListformField.BalanceExceedAction",
"En": "When Balance Is Exceeded",
"Tr": "Bakiye Aşılırsa"
},
{
"FieldName": "IsActive",
"CaptionName": "App.Listform.ListformField.IsActive",
@ -225,6 +241,13 @@
"Value": "true",
"CustomValueType": 1,
"SqlQuery": ""
},
{
"FieldName": "BalanceExceedAction",
"FieldDbType": 16,
"Value": "Block",
"CustomValueType": 1,
"SqlQuery": ""
}
],
"WidgetsDto": [
@ -1977,6 +2000,144 @@
"CreatorId": null,
"Id": "00000000-0000-0000-0000-000000000000"
},
{
"EditType": null,
"ListFormCode": "App.Hr.LeaveType",
"RoleId": null,
"UserId": null,
"ColumnJoinTableDto": {
"JoinType": null,
"TableName": null,
"JoinFieldName": null,
"BaseTableJoinFieldName": null,
"SelectFieldName": null,
"FieldNameAlias": null,
"JoinCondition2": "AND",
"JoinFieldName2": null,
"BaseTableJoinFieldName2_OrValue": null
},
"FieldName": "BalanceExceedAction",
"CaptionName": "App.Listform.ListformField.BalanceExceedAction",
"PlaceHolder": null,
"ReadOnly": false,
"Visible": true,
"IsActive": true,
"Width": 0,
"ListOrderNo": 10,
"SourceDbType": 16,
"CultureName": "en",
"DataType": "string",
"SortIndex": 0,
"SortDirection": null,
"AllowSearch": true,
"AllowEditing": true,
"AllowAdding": true,
"ColumnCssClass": null,
"ColumnCssValue": null,
"BandName": null,
"ColumnFilterDto": {
"AllowFiltering": true,
"FilterOperations": null,
"SelectedFilterOperation": null,
"FilterValue": null
},
"ColumnHeaderDto": {
"AllowHeaderFiltering": true,
"AllowSearch": false,
"DataSource": null,
"DataSourceString": ""
},
"ColumnGroupingDto": {
"AllowGrouping": true,
"GroupIndex": null,
"AutoExpandGroup": false
},
"ColumnCustomizationDto": {
"Fixed": false,
"FixedPosition": "left",
"AllowReordering": true
},
"ColumnTotalSummaryDto": {
"SummaryType": null,
"ValueFormat": null,
"ShowInGroupFooter": false,
"AlignByColumn": false,
"DisplayFormat": null,
"ShowInColumn": null
},
"ColumnGroupSummaryDto": {
"SummaryType": null,
"ValueFormat": null,
"ShowInGroupFooter": false,
"AlignByColumn": false,
"DisplayFormat": null,
"ShowInColumn": null
},
"LookupDto": {
"DataSourceType": 1,
"ValueExpr": "key",
"DisplayExpr": "name",
"CascadeRelationField": null,
"CascadeFilterOperator": "=",
"CascadeParentFields": null,
"CascadeEmptyFields": null,
"LookupQuery": "[{\"key\":\"Block\",\"name\":\"App.Hr.LeaveType.BalanceExceedAction.Block\"},{\"key\":\"Warn\",\"name\":\"App.Hr.LeaveType.BalanceExceedAction.Warn\"}]"
},
"ValidationRuleDto": [
{
"Type": "required",
"Message": null,
"IgnoreEmptyValue": false,
"Reevaluate": true,
"ComparisonType": "",
"Pattern": "",
"Max": null,
"Min": null,
"Trim": true
}
],
"ColumnStylingDto": [],
"PermissionDto": {
"Deny": false,
"C": "App.Hr.LeaveType.Create",
"R": "App.Hr.LeaveType",
"U": "App.Hr.LeaveType.Update",
"E": true,
"I": true
},
"CanRead": false,
"CanCreate": false,
"CanUpdate": false,
"CanExport": false,
"DefaultValue": null,
"DefaultValueType": 0,
"PivotSettingsDto": {
"IsPivot": true,
"Expanded": false,
"SortOrder": "asc",
"Area": "filter",
"Format": null,
"SummaryType": null,
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Format": null,
"EditOrderNo": 9,
"EditGroupOrderNo": 1,
"EditorType2": "dxRadioGroup",
"ColSpan": 1,
"EditorOptions": "{\"layout\":\"horizontal\"}",
"GridBoxOptions": null,
"TagBoxOptions": null,
"ImageUploadOptions": null,
"EditorScript": null,
"LastModificationTime": null,
"LastModifierId": null,
"CreationTime": "0001-01-01T00:00:00",
"CreatorId": null,
"Id": "00000000-0000-0000-0000-000000000000"
},
{
"EditType": null,
"ListFormCode": "App.Hr.LeaveType",
@ -2000,7 +2161,7 @@
"Visible": true,
"IsActive": true,
"Width": 0,
"ListOrderNo": 10,
"ListOrderNo": 11,
"SourceDbType": 3,
"CultureName": "en",
"DataType": "boolean",
@ -2100,7 +2261,7 @@
},
"Alignment": null,
"Format": null,
"EditOrderNo": 9,
"EditOrderNo": 10,
"EditGroupOrderNo": 1,
"EditorType2": "dxCheckBox",
"ColSpan": 1,
@ -2129,7 +2290,10 @@
"App.Listform.ListformField.IsPaid",
"App.Listform.ListformField.DeductsFromAnnualLeave",
"App.Listform.ListformField.RequiresDocument",
"App.Listform.ListformField.AllowHalfDay"
"App.Listform.ListformField.AllowHalfDay",
"App.Listform.ListformField.BalanceExceedAction",
"App.Hr.LeaveType.BalanceExceedAction.Block",
"App.Hr.LeaveType.BalanceExceedAction.Warn"
],
"PermissionGroupNames": [],
"PermissionNames": [

View file

@ -1471,29 +1471,5 @@
"MenuCodes": [],
"DataSourceCodes": [],
"ModuleIds": []
},
"Pending": {
"Since": "2026-10-04T17:38:14.262016+03:00",
"Replaced": {
"LanguageKeys": [
"App.Hr.My360FeedbackScore",
"App.Hr.My360FeedbackScore.Title",
"App.Hr.My360FeedbackScore.Desc"
],
"PermissionGroupNames": [],
"PermissionNames": [
"App.Hr.My360FeedbackScore",
"App.Hr.My360FeedbackScore.Create",
"App.Hr.My360FeedbackScore.Update",
"App.Hr.My360FeedbackScore.Delete",
"App.Hr.My360FeedbackScore.Export",
"App.Hr.My360FeedbackScore.Import",
"App.Hr.My360FeedbackScore.Note"
],
"MenuCodes": [],
"DataSourceCodes": [],
"ModuleIds": []
},
"ListFormCodes": []
}
}

View file

@ -2878,29 +2878,5 @@
"MenuCodes": [],
"DataSourceCodes": [],
"ModuleIds": []
},
"Pending": {
"Since": "2026-10-04T17:38:14.5541252+03:00",
"Replaced": {
"LanguageKeys": [
"App.Hr.MyDevelopmentPlan",
"App.Hr.MyDevelopmentPlan.Title",
"App.Hr.MyDevelopmentPlan.Desc"
],
"PermissionGroupNames": [],
"PermissionNames": [
"App.Hr.MyDevelopmentPlan",
"App.Hr.MyDevelopmentPlan.Create",
"App.Hr.MyDevelopmentPlan.Update",
"App.Hr.MyDevelopmentPlan.Delete",
"App.Hr.MyDevelopmentPlan.Export",
"App.Hr.MyDevelopmentPlan.Import",
"App.Hr.MyDevelopmentPlan.Note"
],
"MenuCodes": [],
"DataSourceCodes": [],
"ModuleIds": []
},
"ListFormCodes": []
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1999,29 +1999,5 @@
"MenuCodes": [],
"DataSourceCodes": [],
"ModuleIds": []
},
"Pending": {
"Since": "2026-10-04T17:38:14.5694024+03:00",
"Replaced": {
"LanguageKeys": [
"App.Hr.MyPerformanceCompetency",
"App.Hr.MyPerformanceCompetency.Title",
"App.Hr.MyPerformanceCompetency.Desc"
],
"PermissionGroupNames": [],
"PermissionNames": [
"App.Hr.MyPerformanceCompetency",
"App.Hr.MyPerformanceCompetency.Create",
"App.Hr.MyPerformanceCompetency.Update",
"App.Hr.MyPerformanceCompetency.Delete",
"App.Hr.MyPerformanceCompetency.Export",
"App.Hr.MyPerformanceCompetency.Import",
"App.Hr.MyPerformanceCompetency.Note"
],
"MenuCodes": [],
"DataSourceCodes": [],
"ModuleIds": []
},
"ListFormCodes": []
}
}

View file

@ -2630,29 +2630,5 @@
"MenuCodes": [],
"DataSourceCodes": [],
"ModuleIds": []
},
"Pending": {
"Since": "2026-10-04T17:38:14.5823584+03:00",
"Replaced": {
"LanguageKeys": [
"App.Hr.MyPerformanceGoal",
"App.Hr.MyPerformanceGoal.Title",
"App.Hr.MyPerformanceGoal.Desc"
],
"PermissionGroupNames": [],
"PermissionNames": [
"App.Hr.MyPerformanceGoal",
"App.Hr.MyPerformanceGoal.Create",
"App.Hr.MyPerformanceGoal.Update",
"App.Hr.MyPerformanceGoal.Delete",
"App.Hr.MyPerformanceGoal.Export",
"App.Hr.MyPerformanceGoal.Import",
"App.Hr.MyPerformanceGoal.Note"
],
"MenuCodes": [],
"DataSourceCodes": [],
"ModuleIds": []
},
"ListFormCodes": []
}
}

View file

@ -3665,31 +3665,5 @@
],
"DataSourceCodes": [],
"ModuleIds": []
},
"Pending": {
"Since": "2026-10-04T17:38:14.6070208+03:00",
"Replaced": {
"LanguageKeys": [
"App.Hr.PerformanceCalibration",
"App.Hr.PerformanceCalibration.Title",
"App.Hr.PerformanceCalibration.Desc"
],
"PermissionGroupNames": [],
"PermissionNames": [
"App.Hr.PerformanceCalibration",
"App.Hr.PerformanceCalibration.Create",
"App.Hr.PerformanceCalibration.Update",
"App.Hr.PerformanceCalibration.Delete",
"App.Hr.PerformanceCalibration.Export",
"App.Hr.PerformanceCalibration.Import",
"App.Hr.PerformanceCalibration.Note"
],
"MenuCodes": [
"App.Hr.PerformanceCalibration"
],
"DataSourceCodes": [],
"ModuleIds": []
},
"ListFormCodes": []
}
}

View file

@ -1481,31 +1481,5 @@
"MenuCodes": [],
"DataSourceCodes": [],
"ModuleIds": []
},
"Pending": {
"Since": "2026-10-04T17:38:14.6309017+03:00",
"Replaced": {
"LanguageKeys": [
"App.Hr.PerformanceFeedbackScore",
"App.Hr.PerformanceFeedbackScore.Title",
"App.Hr.PerformanceFeedbackScore.Desc",
"App.Listform.ListformField.FeedbackId",
"App.Listform.ListformField.LevelId"
],
"PermissionGroupNames": [],
"PermissionNames": [
"App.Hr.PerformanceFeedbackScore",
"App.Hr.PerformanceFeedbackScore.Create",
"App.Hr.PerformanceFeedbackScore.Update",
"App.Hr.PerformanceFeedbackScore.Delete",
"App.Hr.PerformanceFeedbackScore.Export",
"App.Hr.PerformanceFeedbackScore.Import",
"App.Hr.PerformanceFeedbackScore.Note"
],
"MenuCodes": [],
"DataSourceCodes": [],
"ModuleIds": []
},
"ListFormCodes": []
}
}

View file

@ -1915,34 +1915,5 @@
],
"DataSourceCodes": [],
"ModuleIds": []
},
"Pending": {
"Since": "2026-10-04T17:38:14.6542592+03:00",
"Replaced": {
"LanguageKeys": [
"App.Hr.PerformanceRating",
"App.Hr.PerformanceRating.Title",
"App.Hr.PerformanceRating.Desc",
"App.Listform.ListformField.MinScore",
"App.Listform.ListformField.MaxScore",
"App.Listform.ListformField.TargetDistribution"
],
"PermissionGroupNames": [],
"PermissionNames": [
"App.Hr.PerformanceRating",
"App.Hr.PerformanceRating.Create",
"App.Hr.PerformanceRating.Update",
"App.Hr.PerformanceRating.Delete",
"App.Hr.PerformanceRating.Export",
"App.Hr.PerformanceRating.Import",
"App.Hr.PerformanceRating.Note"
],
"MenuCodes": [
"App.Hr.PerformanceRating"
],
"DataSourceCodes": [],
"ModuleIds": []
},
"ListFormCodes": []
}
}

View file

@ -2855,29 +2855,5 @@
"MenuCodes": [],
"DataSourceCodes": [],
"ModuleIds": []
},
"Pending": {
"Since": "2026-10-04T17:38:14.7032225+03:00",
"Replaced": {
"LanguageKeys": [
"App.Hr.TeamDevelopmentPlan",
"App.Hr.TeamDevelopmentPlan.Title",
"App.Hr.TeamDevelopmentPlan.Desc"
],
"PermissionGroupNames": [],
"PermissionNames": [
"App.Hr.TeamDevelopmentPlan",
"App.Hr.TeamDevelopmentPlan.Create",
"App.Hr.TeamDevelopmentPlan.Update",
"App.Hr.TeamDevelopmentPlan.Delete",
"App.Hr.TeamDevelopmentPlan.Export",
"App.Hr.TeamDevelopmentPlan.Import",
"App.Hr.TeamDevelopmentPlan.Note"
],
"MenuCodes": [],
"DataSourceCodes": [],
"ModuleIds": []
},
"ListFormCodes": []
}
}

File diff suppressed because it is too large Load diff

View file

@ -2007,29 +2007,5 @@
"MenuCodes": [],
"DataSourceCodes": [],
"ModuleIds": []
},
"Pending": {
"Since": "2026-10-04T17:38:14.7146321+03:00",
"Replaced": {
"LanguageKeys": [
"App.Hr.TeamPerformanceCompetency",
"App.Hr.TeamPerformanceCompetency.Title",
"App.Hr.TeamPerformanceCompetency.Desc"
],
"PermissionGroupNames": [],
"PermissionNames": [
"App.Hr.TeamPerformanceCompetency",
"App.Hr.TeamPerformanceCompetency.Create",
"App.Hr.TeamPerformanceCompetency.Update",
"App.Hr.TeamPerformanceCompetency.Delete",
"App.Hr.TeamPerformanceCompetency.Export",
"App.Hr.TeamPerformanceCompetency.Import",
"App.Hr.TeamPerformanceCompetency.Note"
],
"MenuCodes": [],
"DataSourceCodes": [],
"ModuleIds": []
},
"ListFormCodes": []
}
}

View file

@ -2608,29 +2608,5 @@
"MenuCodes": [],
"DataSourceCodes": [],
"ModuleIds": []
},
"Pending": {
"Since": "2026-10-04T17:38:14.7265586+03:00",
"Replaced": {
"LanguageKeys": [
"App.Hr.TeamPerformanceGoal",
"App.Hr.TeamPerformanceGoal.Title",
"App.Hr.TeamPerformanceGoal.Desc"
],
"PermissionGroupNames": [],
"PermissionNames": [
"App.Hr.TeamPerformanceGoal",
"App.Hr.TeamPerformanceGoal.Create",
"App.Hr.TeamPerformanceGoal.Update",
"App.Hr.TeamPerformanceGoal.Delete",
"App.Hr.TeamPerformanceGoal.Export",
"App.Hr.TeamPerformanceGoal.Import",
"App.Hr.TeamPerformanceGoal.Note"
],
"MenuCodes": [],
"DataSourceCodes": [],
"ModuleIds": []
},
"ListFormCodes": []
}
}

View file

@ -4498,6 +4498,7 @@ mr-1.4
sozsoft-*
# --- custom component classes ---
!bg-transparent
${className}
absolute
backdrop-blur-sm
bg-amber-50
@ -4512,6 +4513,7 @@ bg-gray-950
bg-gray-950/80
bg-green-700
bg-indigo-50
bg-orange-100
bg-purple-700
bg-red-50
bg-red-600
@ -4520,6 +4522,7 @@ bg-sky-600
bg-slate-100
bg-slate-300
bg-slate-50
bg-teal-600
bg-white
bg-white/10
bg-white/20
@ -4534,6 +4537,7 @@ border-collapse
border-dashed
border-gray-200
border-gray-200/50
border-gray-300
border-indigo-200
border-l-4
border-slate-100
@ -4552,12 +4556,14 @@ dark:bg-gray-900/40
dark:bg-gray-950
dark:bg-gray-950/45
dark:bg-indigo-900/30
dark:bg-orange-900/40
dark:bg-red-950
dark:bg-slate-700
dark:bg-slate-800
dark:bg-slate-900
dark:block
dark:border-amber-800
dark:border-gray-600
dark:border-gray-700
dark:border-gray-700/50
dark:border-gray-800
@ -4568,6 +4574,7 @@ dark:divide-gray-700
dark:from-gray-950
dark:hidden
dark:hover:!bg-transparent
dark:hover:bg-gray-700
dark:hover:bg-slate-800
dark:hover:bg-slate-800/70
dark:hover:text-gray-300
@ -4583,6 +4590,7 @@ dark:text-gray-400
dark:text-green-400
dark:text-indigo-300
dark:text-indigo-400
dark:text-orange-300
dark:text-red-200
dark:text-red-400
dark:text-slate-100
@ -4628,9 +4636,11 @@ h-full
hidden
hover:!bg-transparent
hover:bg-blue-50
hover:bg-gray-50
hover:bg-red-700
hover:bg-sky-700
hover:bg-slate-50
hover:bg-teal-700
hover:bg-white/10
hover:bg-white/20
hover:border-sky-400
@ -4700,6 +4710,7 @@ object-contain
opacity-20
overflow-auto
overflow-hidden
p-2
p-3
p-4
p-5
@ -4714,10 +4725,14 @@ pt-20
pt-3
pt-40
pt-6
px-1.5
px-2
px-3
px-4
px-6
px-8
py-0.5
py-1
py-1.5
py-10
py-12
@ -4728,6 +4743,7 @@ py-3
py-4
py-6
relative
rounded
rounded-2xl
rounded-lg
rounded-md
@ -4738,11 +4754,13 @@ shadow-lg
shadow-md
shadow-sm
shadow-xl
shrink-0
sm:grid-cols-2
space-x-2
space-x-3
space-x-4
space-x-6
space-y-1
space-y-2
space-y-3
space-y-4
@ -4770,6 +4788,8 @@ text-indigo-500
text-indigo-600
text-left
text-lg
text-orange-500
text-orange-700
text-purple-500
text-purple-600
text-red-500
@ -4781,6 +4801,7 @@ text-slate-600
text-slate-700
text-slate-800
text-sm
text-teal-500
text-white
text-xl
text-xs

View file

@ -48,6 +48,8 @@ type NotificationListItem = {
creatorId: string
tenantId?: string
notificationType: string
/** Satır başlığı: kaydın çevrilmiş konusu, yoksa tip adı. */
title: string
message: string
date: string
readed: boolean
@ -113,6 +115,7 @@ const groupNotifications = (items: NotificationDto[]): NotificationListItem[] =>
ids: [item.id],
channels: [item.notificationChannel],
notificationType: item.notificationType,
title: item.subject || item.notificationType,
message: item.message,
date,
creatorId: item.creatorId as string,
@ -239,7 +242,7 @@ const NotificationDropdown = ({ className }: { className?: string }) => {
continue
}
const title = notification.notificationType || APP_NAME
const title = notification.subject || notification.notificationType || APP_NAME
const options: NotificationOptions = {
body: notification.message,
dir: 'ltr',
@ -425,8 +428,8 @@ const NotificationDropdown = ({ className }: { className?: string }) => {
</div>
<div className="ltr:ml-3 rtl:mr-3 ltr:mr-6 rtl:ml-6 min-w-0">
<div>
{item.notificationType && (
<span className="font-semibold heading-text">{item.notificationType} </span>
{item.title && (
<span className="font-semibold heading-text">{item.title} </span>
)}
<div>{item.message}</div>
</div>

View file

@ -4,6 +4,8 @@ export interface NotificationDto {
id: string
notificationChannel: string
notificationType: string
/** Çevrilmiş başlık; boşsa tip adı gösterilir. */
subject?: string | null
message: string
isRead: boolean
/** Kullanıcıya iletildi mi; okundu işareti yalnızca iletilmiş kayıtta yazılabilir. */

View file

@ -13,6 +13,7 @@
* yeniden derlenir.
*/
import apiService from '@/services/api.service'
import { getLocalization } from '@/services/localization.service'
import { store } from '@/store'
@ -120,9 +121,16 @@ const buildHelpers = ({
data[resolveKey(data, field) ?? field] = e.value
}
// Lookup satırı değişen editörün seçimidir. Form olayları (`onFieldDataChanged`, form sayfası)
// bileşen olarak formu verir; formda `selectedItem` olmadığı için seçim alanın editöründen okunur,
// yoksa `copy` / `selected()` sessizce boş kalırdı.
let selectedItem: any = null
try {
selectedItem = e?.component?.option ? e.component.option('selectedItem') : null
const component = e?.component
selectedItem =
component?.option?.('selectedItem') ??
(field ? component?.getEditor?.(field)?.option?.('selectedItem') : null) ??
null
} catch {
selectedItem = null
}
@ -248,7 +256,20 @@ const buildHelpers = ({
const openUrl = (url: string, target: string = '_blank') => window.open(tpl(url), target)
const api = async (url: string, responsePath?: string, options?: Parameters<typeof fetch>[1]) => {
const response = await fetch(tpl(url), options)
const target = tpl(url)
// Uygulamanın kendi ucu oturum ve kiracı başlığı ister; fetch bunları taşımadığı için
// istek platform istemcisinden geçer (ör. ekranın lookup ucu: /api/app/list-form-select/lookup).
if (target.startsWith('/api/')) {
const body = typeof options?.body === 'string' ? JSON.parse(options.body) : options?.body
const response = await apiService.fetchData({
url: target,
method: options?.method ?? 'GET',
data: body as Record<string, unknown> | undefined,
})
return responsePath ? pickPath(response.data, responsePath) : response.data
}
const response = await fetch(target, options)
const json = await response.json()
return responsePath ? pickPath(json, responsePath) : json
}

View file

@ -25,7 +25,7 @@ const Event = ({ data }: EventProps) => {
<>
<div className="my-1 flex flex-wrap items-center">
<div>
<HighlightedText>{data.notificationType}</HighlightedText>{' '}
<HighlightedText>{data.subject || data.notificationType}</HighlightedText>{' '}
{translate('::Abp.Identity.ActivityLogs.SentNotification')}
</div>

View file

@ -73,6 +73,18 @@ export function WorkflowCriteria({
)
const defaultCompareColumn =
numericCompareColumn?.columnName ?? compareColumnOptions[0]?.value ?? 'Price'
// Onaycı kayda göre değişiyorsa (ör. çalışanın departman müdürü) düğüme kullanıcı yerine
// kaydın bir alanı "@Alan" olarak yazılır; motor onaycıyı o alandan okur.
const approverOptions = useMemo(
() => [
...userList,
...selectCommandColumns.map((column) => ({
value: `@${column.columnName}`,
label: translate('::App.ListFormWorkflow.ApproverField', { field: column.columnName }),
})),
],
[selectCommandColumns, translate, userList],
)
React.useEffect(() => {
if (formValues.kind !== 'Compare' || !compareColumnOptions.length) {
@ -273,7 +285,7 @@ export function WorkflowCriteria({
<FormItem asterisk label={translate('::App.Listform.ListformField.Approver')}>
<MultiSelectField
required
options={userList}
options={approverOptions}
value={formValues.approver}
onChange={(value) => setField('approver', value)}
/>

View file

@ -74,6 +74,7 @@ import {
FaPercent,
FaUpload,
FaHourglassHalf,
FaUmbrellaBeach,
} from 'react-icons/fa'
import {
CircleStencil,
@ -96,6 +97,7 @@ import HealthCardTab from './HealthCardTab'
import EducationSkillsTab from './EducationSkillsTab'
import FamilyEmergencyTab from './FamilyEmergencyTab'
import FinancialTab from './FinancialTab'
import LeaveTab from './LeaveTab'
import {
CheckboxField,
DateField,
@ -335,6 +337,9 @@ function UserDetails() {
<TabNav value="health" icon={<FaHeartbeat className="text-sm" />}>
{translate('::Abp.Identity.User.HealthCard')}
</TabNav>
<TabNav value="leaves" icon={<FaUmbrellaBeach className="text-sm" />}>
{translate('::Abp.Identity.User.Leaves')}
</TabNav>
<TabNav value="permission" icon={<FaCheckCircle className="text-sm" />}>
{translate('::Abp.Identity.Permissions')}
</TabNav>
@ -1054,6 +1059,12 @@ function UserDetails() {
</div>
</TabContent>
<TabContent value="leaves">
<div className="px-4 py-2">
<LeaveTab userId={userDetails.id!} />
</div>
</TabContent>
<TabContent value="educationSkills">
<div className="px-4 py-2">
<EducationSkillsTab userDetails={userDetails} onSaved={getUser} />

View file

@ -0,0 +1,110 @@
import PlatformViewHost from '@/components/componentEditor/PlatformViewHost'
import apiService from '@/services/api.service'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { usePermission } from '@/utils/hooks/usePermission'
import { useEffect, useMemo, useState } from 'react'
const BALANCE = 'App.Hr.LeaveBalance'
const REQUESTS = 'App.Hr.LeaveRequest'
const LEDGER = 'App.Hr.LeaveBalanceYear'
type LeaveSummary = {
EntitledDays?: number
AdjustmentDays?: number
UsedDays?: number
PendingDays?: number
RemainingDays?: number
AvailableDays?: number
ServiceYears?: number
NextAccrualDate?: string
}
const formatDays = (value?: number) => {
const number = Number(value ?? 0)
return Number.isInteger(number) ? String(number) : number.toFixed(1)
}
/** İzin özeti İzin Bakiyeleri ekranının sorgusundan okunur; yetki ve kiracı süzmesi ListForm'da kalır. */
function LeaveTab({ userId }: { userId: string }) {
const { translate } = useLocalization()
const { checkPermission } = usePermission()
const [summary, setSummary] = useState<LeaveSummary>()
const canSeeBalance = checkPermission(BALANCE)
// Yeni izin satırının EmployeeId değeri de bu filtreden dolar (applyNewRowDefaults).
const employeeSearchParams = useMemo(
() => new URLSearchParams({ filter: JSON.stringify(['EmployeeId', '=', userId]) }),
[userId],
)
useEffect(() => {
if (!canSeeBalance) return
let active = true
apiService
.fetchData<{ data?: LeaveSummary[] }>({
method: 'GET',
url: '/api/app/list-form-select/select',
params: { listFormCode: BALANCE, filter: JSON.stringify(['Id', '=', userId]), take: 1 },
})
.then((response) => active && setSummary(response.data?.data?.[0]))
.catch(() => active && setSummary(undefined))
return () => {
active = false
}
}, [canSeeBalance, userId])
const field = (name: string) => translate(`::App.Listform.ListformField.${name}`)
const cards: [string, number | undefined, string][] = [
['EntitledDays', summary?.EntitledDays, 'text-gray-900 dark:text-gray-100'],
['AdjustmentDays', summary?.AdjustmentDays, 'text-gray-900 dark:text-gray-100'],
['UsedDays', summary?.UsedDays, 'text-gray-900 dark:text-gray-100'],
['PendingDays', summary?.PendingDays, 'text-orange-500'],
[
'RemainingDays',
summary?.RemainingDays,
Number(summary?.RemainingDays ?? 0) < 0 ? 'text-red-600' : 'text-teal-600',
],
['AvailableDays', summary?.AvailableDays, 'text-indigo-600'],
]
return (
<div className="flex flex-col gap-6">
{canSeeBalance && (
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 xl:grid-cols-6">
{cards.map(([name, value, color]) => (
<div key={name} className="rounded-lg border border-gray-200 p-3 text-center dark:border-gray-700">
<div className={`text-xl font-semibold ${color}`}>{formatDays(value)}</div>
<div className="text-xs text-gray-500 dark:text-gray-400">{field(name)}</div>
</div>
))}
</div>
)}
{checkPermission(REQUESTS) && (
<div>
<h6 className="mb-2">{translate(`::${REQUESTS}`)}</h6>
<PlatformViewHost
view="List"
listFormCode={REQUESTS}
height="360px"
searchParams={employeeSearchParams}
/>
</div>
)}
{checkPermission(LEDGER) && (
<div>
<h6 className="mb-2">{translate(`::${LEDGER}`)}</h6>
<PlatformViewHost
view="List"
listFormCode={LEDGER}
height="320px"
searchParams={employeeSearchParams}
/>
</div>
)}
</div>
)
}
export default LeaveTab

View file

@ -419,6 +419,11 @@ const FormDevExpress = (props: {
onFieldDataChanged={(e: FieldDataChangedEvent) => {
if (!e.dataField) return
// DevExtreme değeri formData nesnesine yerinde yazar; o durumda hasChanges false kalır ve
// state yenilenmez. Script'ler formDataRef'ten çalıştığı için ref her değişimde güncellenir,
// yoksa sonraki commit script'siz alanların değerini (ör. lookup seçimi) eski kopyayla ezerdi.
formDataRef.current = { ...formDataRef.current, [e.dataField]: e.value }
const newFormData = { ...formData, [e.dataField]: e.value }
let hasChanges = !Object.is(formData?.[e.dataField], e.value)