sozsoft-platform/configs/seeds/sql/object/Hr_S_LeaveRequestAfterSave.sql

364 lines
20 KiB
MySQL
Raw Normal View History

2026-10-04 19:17:16 +00:00
CREATE OR ALTER PROCEDURE [dbo].[Hr_S_LeaveRequestAfterSave]
@RequestId uniqueidentifier,
@ActorId uniqueidentifier,
@Actor nvarchar(16)
AS
BEGIN
SET NOCOUNT ON;
2026-10-04 21:18:13 +00:00
-- İzin talebinin her kaydından (ekleme, güncelleme, Grid Workflow kararı, silme) sonra çalışır:
2026-10-04 19:17:16 +00:00
-- çalışan bilgisi, talep no, müdür, gün sayısı tamamlanır; kurallar doğrulanır (THROW kaydı
2026-10-04 21:18:13 +00:00
-- geri alır); onaylı rapor türü Sağlık Kartı istirahatiyle eşlenir; durum değiştiyse bildirim
-- gider; puantaj yeniden hesaplanır.
2026-10-04 19:17:16 +00:00
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),
2026-10-04 21:18:13 +00:00
@LastStatus nvarchar(50), @Description nvarchar(1024), @IsDeleted bit;
2026-10-04 19:17:16 +00:00
SELECT @TenantId = TenantId, @EmployeeId = EmployeeId, @LeaveTypeId = LeaveTypeId,
@StartDate = StartDate, @EndDate = EndDate, @IsHalfDay = IsHalfDay, @HalfDayPart = HalfDayPart,
@Document = DocumentContent, @SickLeaveId = SickLeaveId, @Status = ApprovalStatus,
2026-10-04 21:18:13 +00:00
@LastStatus = LastNotifiedStatus, @Description = [Description], @IsDeleted = IsDeleted
2026-10-04 19:17:16 +00:00
FROM Hr_T_LeaveRequest
2026-10-04 21:18:13 +00:00
WHERE Id = @RequestId;
2026-10-04 19:17:16 +00:00
IF @EmployeeId IS NULL
RETURN;
2026-10-04 21:18:13 +00:00
-- Puantajı kapanmış aya düşen talep eklenemez, değiştirilemez, silinemez (eski tarihleri
-- Hr_S_LeaveRequestGuard denetler).
IF dbo.Hr_F_TimesheetPeriodClosed(@TenantId, CAST(@StartDate AS date), CAST(@EndDate AS date)) = 1
THROW 51118, N'İzin tarihleri kapalı bir puantaj dönemine düşüyor. Önce İnsan Kaynakları dönemi açmalıdır.', 1;
IF @IsDeleted = 1
BEGIN
-- Silinen talebin istirahati de kalkar; rapora denk gelen izin günleri ve puantaj yenilenir.
IF @SickLeaveId IS NOT NULL
BEGIN
UPDATE Adm_T_UserSickLeave
SET IsDeleted = 1, DeletionTime = GETUTCDATE(), DeleterId = @ActorId
WHERE Id = @SickLeaveId AND IsDeleted = 0;
EXEC dbo.Hr_S_LeaveDaysRefresh @TenantId, @EmployeeId;
END
EXEC dbo.Hr_S_TimesheetCalculate @TenantId, @EmployeeId, @StartDate, @EndDate, @ActorId;
RETURN;
END
2026-10-04 19:17:16 +00:00
DECLARE @Message nvarchar(1024);
---------------------------------------------------------------------------------------------
-- Çalışan ve departman müdürü
---------------------------------------------------------------------------------------------
DECLARE @EmployeeName nvarchar(256), @DepartmentId uniqueidentifier, @JobPositionId uniqueidentifier,
2026-10-04 21:18:13 +00:00
@Gender nvarchar(16), @HireDate date, @TerminationDate date;
2026-10-04 19:17:16 +00:00
SELECT @EmployeeName = CONCAT(Name, N' ', Surname), @DepartmentId = DepartmentId,
2026-10-04 21:18:13 +00:00
@JobPositionId = JobPositionId, @Gender = Gender,
@HireDate = CAST(HireDate AS date), @TerminationDate = CAST(TerminationDate AS date)
2026-10-04 19:17:16 +00:00
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
2026-10-19 14:35:08 +00:00
WHERE ListFormCode = N'Hr.LeaveRequest' AND Kind = N'Approval' AND Title = @StatusHr);
2026-10-04 19:17:16 +00:00
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,
2026-10-04 21:18:13 +00:00
@AllowHalfDay = AllowHalfDay, @ExceedAction = BalanceExceedAction
2026-10-04 19:17:16 +00:00
FROM Hr_T_LeaveType
WHERE Id = @LeaveTypeId;
2026-10-04 21:18:13 +00:00
-- Rapor türü kendi çalışma günlerini sayar; diğer türlerde rapora denk gelen gün izinden düşmez.
DECLARE @WorkDays decimal(9, 1) = dbo.Hr_F_LeaveWorkDays(@TenantId, CAST(@StartDate AS date), CAST(@EndDate AS date));
DECLARE @Days decimal(9, 1) = CASE WHEN @TypeCode = N'Sick' THEN @WorkDays
ELSE dbo.Hr_F_LeaveNetDays(@TenantId, @EmployeeId, CAST(@StartDate AS date), CAST(@EndDate AS date)) END;
2026-10-04 19:17:16 +00:00
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;
2026-10-04 21:18:13 +00:00
IF @WorkDays <= 0
2026-10-04 19:17:16 +00:00
THROW 51115, N'Seçilen tarih aralığında çalışma günü yok.', 1;
2026-10-04 21:18:13 +00:00
IF (@HireDate IS NOT NULL AND CAST(@StartDate AS date) < @HireDate)
OR (@TerminationDate IS NOT NULL AND CAST(@EndDate AS date) > @TerminationDate)
THROW 51119, N'İzin tarihleri çalışanın işe giriş ve işten çıkış tarihleri arasında olmalıdır.', 1;
2026-10-04 19:17:16 +00:00
-- Ç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
---------------------------------------------------------------------------------------------
2026-10-04 21:18:13 +00:00
-- 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.
2026-10-04 19:17:16 +00:00
---------------------------------------------------------------------------------------------
2026-10-04 21:18:13 +00:00
IF @TypeCode = N'Sick' AND @Status = @StatusApproved
2026-10-04 19:17:16 +00:00
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
2026-10-04 21:18:13 +00:00
-- Reddedilen, onayı beklemeye dönen ya da türü rapordan başka bir türe çevrilen talebin istirahati kalkar.
2026-10-04 19:17:16 +00:00
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;
2026-10-19 14:35:08 +00:00
DECLARE @Subject nvarchar(128) = CASE WHEN @Status IN (@StatusManager, @StatusHr) THEN N'Hr.Leave.Notification.Approval.Subject' ELSE N'Hr.Leave.Notification.Result.Subject' END;
DECLARE @Body nvarchar(128) = CASE WHEN @Status IN (@StatusManager, @StatusHr) THEN N'Hr.Leave.Notification.Approval.Message' ELSE N'Hr.Leave.Notification.Result.Message' END;
2026-10-04 19:17:16 +00:00
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
2026-10-04 21:18:13 +00:00
---------------------------------------------------------------------------------------------
-- 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 @TypeCode = N'Sick' OR @SickLeaveId IS NOT NULL
EXEC dbo.Hr_S_LeaveDaysRefresh @TenantId, @EmployeeId;
EXEC dbo.Hr_S_TimesheetCalculate @TenantId, @EmployeeId, @StartDate, @EndDate, @ActorId;
2026-10-04 19:17:16 +00:00
END
GO