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