sozsoft-platform/configs/seeds/postgres/object/Hr_S_TimesheetCalculate.sql
2026-10-19 17:35:08 +03:00

461 lines
26 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
$$;