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> GetEmployeesAsync( string cafeId, string? branchId = null, CancellationToken cancellationToken = default); Task GetEmployeeAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default); Task EmployeeBelongsToCafeAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default); Task GetTodayShiftAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default); Task ClockInAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default); Task ClockOutAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default); Task> GetAttendanceAsync(string cafeId, string? employeeId, DateOnly? from, DateOnly? to, CancellationToken cancellationToken = default); Task> GetShiftsAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default); Task> UpsertShiftsAsync(string cafeId, string employeeId, UpsertShiftsRequest request, CancellationToken cancellationToken = default); Task> GetLeaveRequestsAsync(string cafeId, LeaveStatus? status, CancellationToken cancellationToken = default); Task CreateLeaveRequestAsync(string cafeId, string employeeId, CreateLeaveRequest request, CancellationToken cancellationToken = default); Task ReviewLeaveRequestAsync(string cafeId, string leaveId, string reviewerId, ReviewLeaveRequest request, CancellationToken cancellationToken = default); Task> GetSalariesAsync(string cafeId, string? monthYear, CancellationToken cancellationToken = default); Task CreateSalaryAsync(string cafeId, CreateSalaryRequest request, CancellationToken cancellationToken = default); Task 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> 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 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 EmployeeBelongsToCafeAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default) => _db.Employees.AnyAsync(e => e.Id == employeeId && e.CafeId == cafeId, cancellationToken); public async Task 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 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 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> 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> 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> 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> 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 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 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> 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 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 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); }