Files
meezi/src/Meezi.API/Services/HrService.cs
T

337 lines
13 KiB
C#
Raw Normal View History

2026-05-27 21:33:48 +03:30
using Meezi.API.Models.Hr;
using Meezi.Core.Entities;
using Meezi.Core.Enums;
using Meezi.Core.Interfaces;
using Meezi.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace Meezi.API.Services;
public interface IHrService
{
Task<IReadOnlyList<EmployeeSummaryDto>> GetEmployeesAsync(
string cafeId,
string? branchId = null,
CancellationToken cancellationToken = default);
Task<EmployeeSummaryDto?> GetEmployeeAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default);
Task<bool> EmployeeBelongsToCafeAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default);
Task<TodayShiftDto?> GetTodayShiftAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default);
Task<AttendanceDto?> ClockInAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default);
Task<AttendanceDto?> ClockOutAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default);
Task<IReadOnlyList<AttendanceDto>> GetAttendanceAsync(string cafeId, string? employeeId, DateOnly? from, DateOnly? to, CancellationToken cancellationToken = default);
Task<IReadOnlyList<ShiftDto>> GetShiftsAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default);
Task<IReadOnlyList<ShiftDto>> UpsertShiftsAsync(string cafeId, string employeeId, UpsertShiftsRequest request, CancellationToken cancellationToken = default);
Task<IReadOnlyList<LeaveRequestDto>> GetLeaveRequestsAsync(string cafeId, LeaveStatus? status, CancellationToken cancellationToken = default);
Task<LeaveRequestDto?> CreateLeaveRequestAsync(string cafeId, string employeeId, CreateLeaveRequest request, CancellationToken cancellationToken = default);
Task<LeaveRequestDto?> ReviewLeaveRequestAsync(string cafeId, string leaveId, string reviewerId, ReviewLeaveRequest request, CancellationToken cancellationToken = default);
Task<IReadOnlyList<EmployeeSalaryDto>> GetSalariesAsync(string cafeId, string? monthYear, CancellationToken cancellationToken = default);
Task<EmployeeSalaryDto?> CreateSalaryAsync(string cafeId, CreateSalaryRequest request, CancellationToken cancellationToken = default);
Task<EmployeeSalaryDto?> MarkSalaryPaidAsync(string cafeId, string salaryId, CancellationToken cancellationToken = default);
}
public class HrService : IHrService
{
private readonly AppDbContext _db;
public HrService(AppDbContext db)
{
_db = db;
}
public async Task<IReadOnlyList<EmployeeSummaryDto>> GetEmployeesAsync(
string cafeId,
string? branchId = null,
CancellationToken cancellationToken = default)
{
var query = _db.Employees.Where(e => e.CafeId == cafeId);
if (!string.IsNullOrEmpty(branchId))
query = query.Where(e => e.BranchId == branchId);
var list = await query.OrderBy(e => e.Name).ToListAsync(cancellationToken);
return list.Select(e => new EmployeeSummaryDto(e.Id, e.Name, e.Phone, e.Role, e.BaseSalary)).ToList();
}
public async Task<EmployeeSummaryDto?> GetEmployeeAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default)
{
var e = await _db.Employees.FirstOrDefaultAsync(x => x.Id == employeeId && x.CafeId == cafeId, cancellationToken);
return e is null ? null : new EmployeeSummaryDto(e.Id, e.Name, e.Phone, e.Role, e.BaseSalary);
}
public Task<bool> EmployeeBelongsToCafeAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default) =>
_db.Employees.AnyAsync(e => e.Id == employeeId && e.CafeId == cafeId, cancellationToken);
public async Task<TodayShiftDto?> GetTodayShiftAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default)
{
if (!await EmployeeBelongsToCafeAsync(cafeId, employeeId, cancellationToken))
return null;
var day = (int)DateTime.UtcNow.DayOfWeek;
var shift = await _db.EmployeeSchedules
.FirstOrDefaultAsync(s => s.EmployeeId == employeeId && s.DayOfWeek == day, cancellationToken);
var type = shift?.ShiftType ?? ShiftType.DayOff;
return new TodayShiftDto(type, ShiftLabel(type));
}
public async Task<AttendanceDto?> ClockInAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default)
{
var employee = await _db.Employees
.FirstOrDefaultAsync(e => e.Id == employeeId && e.CafeId == cafeId, cancellationToken);
if (employee is null) return null;
var today = DateOnly.FromDateTime(DateTime.UtcNow);
var attendance = await _db.Attendances
.FirstOrDefaultAsync(a => a.EmployeeId == employeeId && a.Date == today, cancellationToken);
if (attendance is null)
{
attendance = new Attendance
{
EmployeeId = employeeId,
Date = today,
ClockIn = DateTime.UtcNow
};
_db.Attendances.Add(attendance);
}
else if (attendance.ClockIn is null)
{
attendance.ClockIn = DateTime.UtcNow;
}
else
{
return ToAttendanceDto(attendance, employee.Name);
}
await _db.SaveChangesAsync(cancellationToken);
return ToAttendanceDto(attendance, employee.Name);
}
public async Task<AttendanceDto?> ClockOutAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default)
{
var employee = await _db.Employees
.FirstOrDefaultAsync(e => e.Id == employeeId && e.CafeId == cafeId, cancellationToken);
if (employee is null) return null;
var today = DateOnly.FromDateTime(DateTime.UtcNow);
var attendance = await _db.Attendances
.FirstOrDefaultAsync(a => a.EmployeeId == employeeId && a.Date == today, cancellationToken);
if (attendance?.ClockIn is null) return null;
attendance.ClockOut = DateTime.UtcNow;
await _db.SaveChangesAsync(cancellationToken);
return ToAttendanceDto(attendance, employee.Name);
}
public async Task<IReadOnlyList<AttendanceDto>> GetAttendanceAsync(
string cafeId,
string? employeeId,
DateOnly? from,
DateOnly? to,
CancellationToken cancellationToken = default)
{
var query = _db.Attendances
.Include(a => a.Employee)
.Where(a => a.Employee.CafeId == cafeId);
if (!string.IsNullOrEmpty(employeeId))
query = query.Where(a => a.EmployeeId == employeeId);
if (from.HasValue)
query = query.Where(a => a.Date >= from.Value);
if (to.HasValue)
query = query.Where(a => a.Date <= to.Value);
var list = await query.OrderByDescending(a => a.Date).Take(100).ToListAsync(cancellationToken);
return list.Select(a => ToAttendanceDto(a, a.Employee.Name)).ToList();
}
public async Task<IReadOnlyList<ShiftDto>> GetShiftsAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default)
{
if (!await EmployeeBelongsToCafeAsync(cafeId, employeeId, cancellationToken))
return [];
var shifts = await _db.EmployeeSchedules
.Where(s => s.EmployeeId == employeeId)
.OrderBy(s => s.DayOfWeek)
.ToListAsync(cancellationToken);
return shifts.Select(s => new ShiftDto(s.DayOfWeek, s.ShiftType)).ToList();
}
public async Task<IReadOnlyList<ShiftDto>> UpsertShiftsAsync(
string cafeId,
string employeeId,
UpsertShiftsRequest request,
CancellationToken cancellationToken = default)
{
if (!await EmployeeBelongsToCafeAsync(cafeId, employeeId, cancellationToken))
return [];
var existing = await _db.EmployeeSchedules.Where(s => s.EmployeeId == employeeId).ToListAsync(cancellationToken);
_db.EmployeeSchedules.RemoveRange(existing);
foreach (var s in request.Shifts)
{
_db.EmployeeSchedules.Add(new EmployeeSchedule
{
EmployeeId = employeeId,
DayOfWeek = s.DayOfWeek,
ShiftType = s.ShiftType
});
}
await _db.SaveChangesAsync(cancellationToken);
return request.Shifts.ToList();
}
public async Task<IReadOnlyList<LeaveRequestDto>> GetLeaveRequestsAsync(
string cafeId,
LeaveStatus? status,
CancellationToken cancellationToken = default)
{
var query = _db.LeaveRequests
.Include(l => l.Employee)
.Where(l => l.Employee.CafeId == cafeId);
if (status.HasValue)
query = query.Where(l => l.Status == status.Value);
var list = await query.OrderByDescending(l => l.CreatedAt).ToListAsync(cancellationToken);
return list.Select(l => ToLeaveDto(l)).ToList();
}
public async Task<LeaveRequestDto?> CreateLeaveRequestAsync(
string cafeId,
string employeeId,
CreateLeaveRequest request,
CancellationToken cancellationToken = default)
{
if (!await EmployeeBelongsToCafeAsync(cafeId, employeeId, cancellationToken))
return null;
var employee = await _db.Employees.FindAsync([employeeId], cancellationToken);
var entity = new LeaveRequest
{
EmployeeId = employeeId,
StartDate = request.StartDate,
EndDate = request.EndDate,
Reason = request.Reason,
Status = LeaveStatus.Pending
};
_db.LeaveRequests.Add(entity);
await _db.SaveChangesAsync(cancellationToken);
return ToLeaveDto(entity, employee!.Name);
}
public async Task<LeaveRequestDto?> ReviewLeaveRequestAsync(
string cafeId,
string leaveId,
string reviewerId,
ReviewLeaveRequest request,
CancellationToken cancellationToken = default)
{
var entity = await _db.LeaveRequests
.Include(l => l.Employee)
.FirstOrDefaultAsync(l => l.Id == leaveId && l.Employee.CafeId == cafeId, cancellationToken);
if (entity is null) return null;
entity.Status = request.Status;
entity.ReviewedBy = reviewerId;
await _db.SaveChangesAsync(cancellationToken);
return ToLeaveDto(entity);
}
public async Task<IReadOnlyList<EmployeeSalaryDto>> GetSalariesAsync(
string cafeId,
string? monthYear,
CancellationToken cancellationToken = default)
{
var query = _db.EmployeeSalaries
.Include(s => s.Employee)
.Where(s => s.Employee.CafeId == cafeId);
if (!string.IsNullOrEmpty(monthYear))
query = query.Where(s => s.MonthYear == monthYear);
var list = await query.OrderByDescending(s => s.MonthYear).ToListAsync(cancellationToken);
return list.Select(s => ToSalaryDto(s)).ToList();
}
public async Task<EmployeeSalaryDto?> CreateSalaryAsync(
string cafeId,
CreateSalaryRequest request,
CancellationToken cancellationToken = default)
{
if (!await EmployeeBelongsToCafeAsync(cafeId, request.EmployeeId, cancellationToken))
return null;
var employee = await _db.Employees.FindAsync([request.EmployeeId], cancellationToken);
var net = request.BaseSalary + request.OvertimePay - request.Deductions;
var entity = new EmployeeSalary
{
EmployeeId = request.EmployeeId,
MonthYear = request.MonthYear,
BaseSalary = request.BaseSalary,
OvertimePay = request.OvertimePay,
Deductions = request.Deductions,
NetSalary = net,
IsPaid = false
};
_db.EmployeeSalaries.Add(entity);
await _db.SaveChangesAsync(cancellationToken);
return ToSalaryDto(entity, employee!.Name);
}
public async Task<EmployeeSalaryDto?> MarkSalaryPaidAsync(string cafeId, string salaryId, CancellationToken cancellationToken = default)
{
var entity = await _db.EmployeeSalaries
.Include(s => s.Employee)
.FirstOrDefaultAsync(s => s.Id == salaryId && s.Employee.CafeId == cafeId, cancellationToken);
if (entity is null) return null;
entity.IsPaid = true;
await _db.SaveChangesAsync(cancellationToken);
return ToSalaryDto(entity);
}
private static string ShiftLabel(ShiftType type) => type switch
{
ShiftType.Morning => "08:00 - 16:00",
ShiftType.Evening => "16:00 - 00:00",
_ => "Day off"
};
private static AttendanceDto ToAttendanceDto(Attendance a, string name) => new(
a.Id, a.EmployeeId, name, a.Date, a.ClockIn, a.ClockOut, a.Notes);
private static LeaveRequestDto ToLeaveDto(LeaveRequest l, string? name = null) => new(
l.Id,
l.EmployeeId,
name ?? l.Employee?.Name ?? "",
l.StartDate,
l.EndDate,
l.Reason,
l.Status,
l.ReviewedBy,
l.CreatedAt);
private static EmployeeSalaryDto ToSalaryDto(EmployeeSalary s, string? name = null) => new(
s.Id,
s.EmployeeId,
name ?? s.Employee?.Name ?? "",
s.MonthYear,
s.BaseSalary,
s.OvertimePay,
s.Deductions,
s.NetSalary,
s.IsPaid);
}