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