diff --git a/BMA.EHR.Application/Repositories/Leaves/TimeAttendants/UserDutyTimeRepository.cs b/BMA.EHR.Application/Repositories/Leaves/TimeAttendants/UserDutyTimeRepository.cs index 48bdd0da..3eefefe0 100644 --- a/BMA.EHR.Application/Repositories/Leaves/TimeAttendants/UserDutyTimeRepository.cs +++ b/BMA.EHR.Application/Repositories/Leaves/TimeAttendants/UserDutyTimeRepository.cs @@ -62,7 +62,7 @@ namespace BMA.EHR.Application.Repositories.Leaves.TimeAttendants { var data = _dbContext.Set() .Where(u => !u.IsProcess) - .Where(u => u.EffectiveDate.Value.Date <= DateTime.Now.Date) + .Where(u => u.EffectiveDate.Value.Date <= DateTime.Now.AddHours(7).Date) .ToList(); foreach (var d in data) @@ -106,6 +106,7 @@ namespace BMA.EHR.Application.Repositories.Leaves.TimeAttendants var data = await _dbContext.Set() .Where(x => x.ProfileId == profileId) .Where(x => x.IsProcess) + .Where(x => x.EffectiveDate.Value.Date <= DateTime.Now.Date) .OrderByDescending(x => x.EffectiveDate) .FirstOrDefaultAsync(); diff --git a/BMA.EHR.Domain/Extensions/DateTimeExtension.cs b/BMA.EHR.Domain/Extensions/DateTimeExtension.cs index 18f03079..27ebc5a8 100644 --- a/BMA.EHR.Domain/Extensions/DateTimeExtension.cs +++ b/BMA.EHR.Domain/Extensions/DateTimeExtension.cs @@ -408,6 +408,22 @@ namespace BMA.EHR.Domain.Extensions public int days { get; set; } } + // แปลงจาก DayOfWeek เป็นภาษาไทย + public static string GetThaiDayOfWeek(this DateTime date) + { + return date.DayOfWeek switch + { + DayOfWeek.Sunday => "อาทิตย์", + DayOfWeek.Monday => "จันทร์", + DayOfWeek.Tuesday => "อังคาร", + DayOfWeek.Wednesday => "พุธ", + DayOfWeek.Thursday => "พฤหัสบดี", + DayOfWeek.Friday => "ศุกร์", + DayOfWeek.Saturday => "เสาร์", + _ => "ไม่ทราบ" + }; + } + #endregion #endregion diff --git a/BMA.EHR.Leave/Controllers/LeaveReportController.cs b/BMA.EHR.Leave/Controllers/LeaveReportController.cs index 1568fdeb..d434c520 100644 --- a/BMA.EHR.Leave/Controllers/LeaveReportController.cs +++ b/BMA.EHR.Leave/Controllers/LeaveReportController.cs @@ -71,6 +71,28 @@ namespace BMA.EHR.Leave.Service.Controllers _httpContextAccessor = httpContextAccessor; _permission = permission; } + private class LoopDate + { + public DateTime date { get; set; } + + public bool isHoliday { get; set; } + } + private class DateResultReport + { + public int no { get; set; } + + public string fullName { get; set; } + public string dutyTimeName { get; set; } + public string checkInLocation { get; set; } + public string checkInTime { get; set; } + public string checkOutLocation { get; set; } + public string checkOutTime { get; set; } + public string remark { get; set; } + public string checkInDate { get; set; } + public string checkedOutDate { get; set; } + public DateTime? checkInTimeRaw { get; set; } + public DateTime? checkOutTimeRaw { get; set; } + } #endregion @@ -808,7 +830,7 @@ namespace BMA.EHR.Leave.Service.Controllers var therapyDay = leaveDays.FirstOrDefault(x => x.KeycloakUserId == keycloakUserId && x.LeaveTypeCode == "LV-011"); var therapyDayCount = therapyDay != null ? therapyDay.SumLeaveDay : 0; - var timeStamps = await _processUserTimeStampRepository.GetTimeStampHistoryByRangeForUserAsync(p.Keycloak ?? Guid.Empty,req.StartDate,req.EndDate); + var timeStamps = await _processUserTimeStampRepository.GetTimeStampHistoryByRangeForUserAsync(p.Keycloak ?? Guid.Empty, req.StartDate, req.EndDate); var defaultRound = await _dutyTimeRepository.GetDefaultAsync(); if (defaultRound == null) @@ -822,27 +844,27 @@ namespace BMA.EHR.Leave.Service.Controllers var duty = userRound ?? defaultRound; - /* var processTimeStamps = timeStamps - .Select(d => new - { - d.Id, - CheckInStatus = DateTime.Parse(d.CheckIn.ToString("yyyy-MM-dd HH:mm")) > - DateTime.Parse($"{d.CheckIn.Date.ToString("yyyy-MM-dd")} {duty.StartTimeMorning}") ? - "LATE" : - "NORMAL", - CheckOutStatus = d.CheckOut == null ? "" : - DateTime.Parse(d.CheckOut.Value.ToString("yyyy-MM-dd HH:mm")) < - DateTime.Parse($"{d.CheckIn.Date.ToString("yyyy-MM-dd")} {duty.EndTimeAfternoon}") ? - "LATE" : - DateTime.Parse(d.CheckOut.Value.ToString("yyyy-MM-dd HH:mm")) < - DateTime.Parse($"{d.CheckIn.Date.ToString("yyyy-MM-dd")} {duty.EndTimeMorning}") ? - "ABSENT" : - "NORMAL", - });*/ + /* var processTimeStamps = timeStamps + .Select(d => new + { + d.Id, + CheckInStatus = DateTime.Parse(d.CheckIn.ToString("yyyy-MM-dd HH:mm")) > + DateTime.Parse($"{d.CheckIn.Date.ToString("yyyy-MM-dd")} {duty.StartTimeMorning}") ? + "LATE" : + "NORMAL", + CheckOutStatus = d.CheckOut == null ? "" : + DateTime.Parse(d.CheckOut.Value.ToString("yyyy-MM-dd HH:mm")) < + DateTime.Parse($"{d.CheckIn.Date.ToString("yyyy-MM-dd")} {duty.EndTimeAfternoon}") ? + "LATE" : + DateTime.Parse(d.CheckOut.Value.ToString("yyyy-MM-dd HH:mm")) < + DateTime.Parse($"{d.CheckIn.Date.ToString("yyyy-MM-dd")} {duty.EndTimeMorning}") ? + "ABSENT" : + "NORMAL", + });*/ /*var absentCount = processTimeStamps.Count(x => x.CheckOutStatus == "ABSENT"); var lateCount = processTimeStamps.Count(x => x.CheckInStatus == "LATE");*/ - + var absentCount = timeStamps.Count(d => d.CheckOutStatus == "ABSENT"); // นับจำนวนที่มี CheckOutStatus == "ABSENT" var lateCount = timeStamps.Count(d => @@ -1151,15 +1173,28 @@ namespace BMA.EHR.Leave.Service.Controllers var weekend = _holidayRepository.GetWeekEnd(req.StartDate.Date, req.EndDate.Date); var excludeDates = holidays.Union(weekend).ToList(); - var dateList = new List(); - + var dateList = new List(); for (DateTime i = req.StartDate.Date; i <= req.EndDate.Date; i = i.AddDays(1)) { - if (!excludeDates.Contains(i)) - dateList.Add(i); + if (excludeDates.Contains(i)) + { + dateList.Add(new LoopDate + { + date = i, + isHoliday = true, + }); + } + else + { + dateList.Add(new LoopDate + { + date = i, + isHoliday = false, + }); + } } - var employees = new List(); + var employees = new List(); var count = 1; var restTotal = 0; @@ -1175,7 +1210,7 @@ namespace BMA.EHR.Leave.Service.Controllers { var keycloakUserId = p.Keycloak ?? Guid.Empty; - var timeStamps = await _processUserTimeStampRepository.GetTimestampByDateAsync(keycloakUserId, dd); + var timeStamps = await _processUserTimeStampRepository.GetTimestampByDateAsync(keycloakUserId, dd.date); var fullName = $"{p.Prefix}{p.FirstName} {p.LastName}"; // _userProfileRepository.GetUserFullName(keycloakUserId, AccessToken); @@ -1192,7 +1227,7 @@ namespace BMA.EHR.Leave.Service.Controllers var duty = userRound ?? defaultRound; // check วันลาของแต่ละคน - var leaveReq = await _leaveRequestRepository.GetLeavePeriodAsync(keycloakUserId, dd); + var leaveReq = await _leaveRequestRepository.GetLeavePeriodAsync(keycloakUserId, dd.date); var remarkStr = string.Empty; if (leaveReq != null) @@ -1218,8 +1253,14 @@ namespace BMA.EHR.Leave.Service.Controllers { if (timeStamps == null) { - if (dd <= DateTime.Now.Date) + if (dd.date <= DateTime.Now.Date) + { remarkStr = "ขาดราชการ"; + if (dd.isHoliday == true) + { + remarkStr = "วันหยุด"; + } + } else remarkStr = ""; } else @@ -1228,7 +1269,7 @@ namespace BMA.EHR.Leave.Service.Controllers if (timeStamps.CheckOut != null) { if (timeStamps.CheckOutStatus == "ABSENT") - remarkStr = "ขาดราชการ" + (!timeStamps.IsLocationCheckOut ? $" (นอกสถานที่:{ timeStamps.CheckOutLocationName })".Trim() : "") ; + remarkStr = "ขาดราชการ" + (!timeStamps.IsLocationCheckOut ? $" (นอกสถานที่:{timeStamps.CheckOutLocationName})".Trim() : ""); else if (timeStamps.CheckInStatus == "ABSENT") remarkStr = "ขาดราชการ" + (!timeStamps.IsLocationCheckIn ? $" (นอกสถานที่:{timeStamps.CheckInLocationName})".Trim() : ""); else if (timeStamps.CheckInStatus == "LATE") @@ -1237,7 +1278,8 @@ namespace BMA.EHR.Leave.Service.Controllers lateTotal += 1; } else - remarkStr = ""; + remarkStr = !timeStamps.IsLocationCheckIn ? $" นอกสถานที่:{timeStamps.CheckInLocationName}".Trim() : ""; + //remarkStr = ""; } else { @@ -1249,12 +1291,13 @@ namespace BMA.EHR.Leave.Service.Controllers lateTotal += 1; } else - remarkStr = ""; + //remarkStr = ""; + remarkStr = !timeStamps.IsLocationCheckIn ? $" นอกสถานที่:{timeStamps.CheckInLocationName}".Trim() : ""; } } } - var emp = new + var emp = new DateResultReport { no = count, fullName = fullName, @@ -1267,11 +1310,13 @@ namespace BMA.EHR.Leave.Service.Controllers $"{timeStamps.CheckOut.Value.ToString("HH:mm")} น." : "", remark = remarkStr, - checkInDate = timeStamps == null ? dd.Date.ToThaiFullDate2().ToThaiNumber() : timeStamps.CheckIn.Date.ToThaiFullDate2().ToThaiNumber(), - checkedOutDate = timeStamps == null ? dd.Date.ToThaiFullDate2().ToThaiNumber() : + checkInDate = timeStamps == null ? dd.date.Date.ToThaiFullDate2() : timeStamps.CheckIn.Date.ToThaiFullDate2(), + checkedOutDate = timeStamps == null ? dd.date.Date.ToThaiFullDate2() : timeStamps.CheckOut != null ? - timeStamps.CheckOut.Value.ToThaiFullDate2().ToThaiNumber() : + timeStamps.CheckOut.Value.ToThaiFullDate2() : "", + checkInTimeRaw = timeStamps?.CheckIn, + checkOutTimeRaw = timeStamps?.CheckOut, }; if (timeStamps != null) @@ -1307,11 +1352,16 @@ namespace BMA.EHR.Leave.Service.Controllers count++; } } + employees = employees.OrderBy(x => x.checkInTimeRaw ?? DateTime.MaxValue).ThenBy(x => x.checkOutTimeRaw ?? DateTime.MaxValue).ToList(); + for (int i = 0; i < employees.Count; i++) + { + employees[i].no = i + 1; + } - var enddate = req.EndDate.Date == req.StartDate.Date ? "" : $" - {req.EndDate.Date.ToThaiShortDate().ToThaiNumber()}"; + var enddate = req.EndDate.Date == req.StartDate.Date ? "" : $" - {req.EndDate.Date.ToThaiShortDate()}"; var item = new { - dateTimeStamp = $"ณ วันที่ {req.StartDate.Date.ToThaiShortDate().ToThaiNumber()}{enddate}", + dateTimeStamp = $"ณ วัน{req.StartDate.Date.GetThaiDayOfWeek()} ที่ {req.StartDate.Date.ToThaiShortDate()}{enddate}", organizationName = profile?.FirstOrDefault()?.Oc ?? "", officerTotal = profile.Count, workTotal = workTotal, @@ -1323,7 +1373,7 @@ namespace BMA.EHR.Leave.Service.Controllers studyTotal = studyTotal, employees = employees }; - + //วันที่ออก var result = new { template = "TimeStamp", diff --git a/BMA.EHR.Leave/Dockerfile b/BMA.EHR.Leave/Dockerfile index 3c211ab3..2e851b2c 100644 --- a/BMA.EHR.Leave/Dockerfile +++ b/BMA.EHR.Leave/Dockerfile @@ -1,6 +1,11 @@ -#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. +#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base + +# ตั้งค่า TimeZone ใน Container +ENV TZ=Asia/Bangkok +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + WORKDIR /app EXPOSE 80 EXPOSE 443 diff --git a/BMA.EHR.Leave/Program.cs b/BMA.EHR.Leave/Program.cs index 44bdd418..82ae3aea 100644 --- a/BMA.EHR.Leave/Program.cs +++ b/BMA.EHR.Leave/Program.cs @@ -24,163 +24,167 @@ using BMA.EHR.Application.Repositories.Leaves.TimeAttendants; using BMA.EHR.Leave.Service.Extensions; var builder = WebApplication.CreateBuilder(args); +// ตั้ง TimeZone เป็น Asia/Bangkok ในโค้ด +var bangkokTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Asia/Bangkok"); +TimeZoneInfo.ClearCachedData(); + + +var issuer = builder.Configuration["Jwt:Issuer"]; +var key = builder.Configuration["Jwt:Key"]; + + +IdentityModelEventSource.ShowPII = true; + +builder.Services.AddHttpContextAccessor(); + +builder.Services.AddApiVersioning(opt => { - var issuer = builder.Configuration["Jwt:Issuer"]; - var key = builder.Configuration["Jwt:Key"]; + opt.DefaultApiVersion = new ApiVersion(1, 0); + opt.AssumeDefaultVersionWhenUnspecified = true; + opt.ReportApiVersions = true; + opt.ApiVersionReader = ApiVersionReader.Combine(new UrlSegmentApiVersionReader(), + new HeaderApiVersionReader("x-api-version"), + new MediaTypeApiVersionReader("x-api-version")); +}); +builder.Services.AddVersionedApiExplorer(setup => +{ + setup.GroupNameFormat = "'v'VVV"; + setup.SubstituteApiVersionInUrl = true; +}); - IdentityModelEventSource.ShowPII = true; +builder.Services.AddEndpointsApiExplorer(); - builder.Services.AddHttpContextAccessor(); - - builder.Services.AddApiVersioning(opt => +// Authorization +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(opt => +{ + opt.SaveToken = true; + opt.RequireHttpsMetadata = false; //false for dev + opt.Authority = issuer; + opt.TokenValidationParameters = new() { - opt.DefaultApiVersion = new ApiVersion(1, 0); - opt.AssumeDefaultVersionWhenUnspecified = true; - opt.ReportApiVersions = true; - opt.ApiVersionReader = ApiVersionReader.Combine(new UrlSegmentApiVersionReader(), - new HeaderApiVersionReader("x-api-version"), - new MediaTypeApiVersionReader("x-api-version")); - }); + ValidateIssuer = true, + ValidateAudience = false, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = issuer, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)) + }; +}); +builder.Services.AddAuthorization(); - builder.Services.AddVersionedApiExplorer(setup => - { - setup.GroupNameFormat = "'v'VVV"; - setup.SubstituteApiVersionInUrl = true; - }); +// use serilog +ConfigureLogs(); +builder.Host.UseSerilog(); - builder.Services.AddEndpointsApiExplorer(); - - // Authorization - builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(opt => - { - opt.SaveToken = true; - opt.RequireHttpsMetadata = false; //false for dev - opt.Authority = issuer; - opt.TokenValidationParameters = new() - { - ValidateIssuer = true, - ValidateAudience = false, - ValidateLifetime = true, - ValidateIssuerSigningKey = true, - ValidIssuer = issuer, - IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)) - }; - }); - builder.Services.AddAuthorization(); - - // use serilog - ConfigureLogs(); - builder.Host.UseSerilog(); - - // Add config CORS - builder.Services.AddCors(options => options.AddDefaultPolicy(builder => - { - builder - .AllowAnyOrigin() - .AllowAnyMethod() - .AllowAnyHeader() - .SetIsOriginAllowedToAllowWildcardSubdomains(); - })); +// Add config CORS +builder.Services.AddCors(options => options.AddDefaultPolicy(builder => +{ + builder + .AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader() + .SetIsOriginAllowedToAllowWildcardSubdomains(); +})); - // Add services to the container. - builder.Services.AddApplication(); - builder.Services.AddLeaveApplication(); - builder.Services.AddPersistence(builder.Configuration); - builder.Services.AddLeavePersistence(builder.Configuration); +// Add services to the container. +builder.Services.AddApplication(); +builder.Services.AddLeaveApplication(); +builder.Services.AddPersistence(builder.Configuration); +builder.Services.AddLeavePersistence(builder.Configuration); - builder.Services.AddHttpClient(); +builder.Services.AddHttpClient(); - builder.Services.AddControllers(options => - { - options.SuppressAsyncSuffixInActionNames = false; - }) - .AddNewtonsoftJson(x => x.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore); +builder.Services.AddControllers(options => +{ + options.SuppressAsyncSuffixInActionNames = false; +}) +.AddNewtonsoftJson(x => x.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore); - builder.Services.AddSwaggerGen(); - builder.Services.ConfigureOptions(); +builder.Services.AddSwaggerGen(); +builder.Services.ConfigureOptions(); - builder.Services.AddHealthChecks(); +builder.Services.AddHealthChecks(); - builder.Services.AddRabbitMqConnectionPooling(builder.Configuration); +builder.Services.AddRabbitMqConnectionPooling(builder.Configuration); - // Add Hangfire services. - var defaultConnection = builder.Configuration.GetConnectionString("DefaultConnection"); +// Add Hangfire services. +var defaultConnection = builder.Configuration.GetConnectionString("DefaultConnection"); + +builder.Services.AddHangfire(configuration => configuration + .SetDataCompatibilityLevel(CompatibilityLevel.Version_170) + .UseSimpleAssemblyNameTypeSerializer() + .UseRecommendedSerializerSettings() + .UseStorage( + new MySqlStorage( + defaultConnection, + new MySqlStorageOptions + { + TransactionIsolationLevel = IsolationLevel.ReadCommitted, + QueuePollInterval = TimeSpan.FromSeconds(15), + JobExpirationCheckInterval = TimeSpan.FromHours(1), + CountersAggregateInterval = TimeSpan.FromMinutes(5), + PrepareSchemaIfNecessary = true, + DashboardJobListLimit = 50000, + TransactionTimeout = TimeSpan.FromMinutes(1), + TablesPrefix = "Hangfire" + }))); +builder.Services.AddHangfireServer(); - builder.Services.AddHangfire(configuration => configuration - .SetDataCompatibilityLevel(CompatibilityLevel.Version_170) - .UseSimpleAssemblyNameTypeSerializer() - .UseRecommendedSerializerSettings() - .UseStorage( - new MySqlStorage( - defaultConnection, - new MySqlStorageOptions - { - TransactionIsolationLevel = IsolationLevel.ReadCommitted, - QueuePollInterval = TimeSpan.FromSeconds(15), - JobExpirationCheckInterval = TimeSpan.FromHours(1), - CountersAggregateInterval = TimeSpan.FromMinutes(5), - PrepareSchemaIfNecessary = true, - DashboardJobListLimit = 50000, - TransactionTimeout = TimeSpan.FromMinutes(1), - TablesPrefix = "Hangfire" - }))); - builder.Services.AddHangfireServer(); -} var app = builder.Build(); + +var apiVersionDescriptionProvider = app.Services.GetRequiredService(); + +if (app.Environment.IsDevelopment()) { - var apiVersionDescriptionProvider = app.Services.GetRequiredService(); - - if (app.Environment.IsDevelopment()) + app.UseSwagger(); + app.UseSwaggerUI(options => { - app.UseSwagger(); - app.UseSwaggerUI(options => + foreach (var description in apiVersionDescriptionProvider.ApiVersionDescriptions) { - foreach (var description in apiVersionDescriptionProvider.ApiVersionDescriptions) - { - options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", - description.GroupName.ToUpperInvariant()); - } - }); - } - - app.MapHealthChecks("/health"); - - - app.UseHttpsRedirection(); - app.UseCors(); - app.UseAuthentication(); - app.UseAuthorization(); - app.UseDefaultFiles(); - app.UseStaticFiles(); - app.MapControllers(); - app.UseMiddleware(); - - - app.UseHangfireDashboard("/hangfire", new DashboardOptions() - { - Authorization = new[] { new CustomAuthorizeFilter() } + options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", + description.GroupName.ToUpperInvariant()); + } }); - - var manager = new RecurringJobManager(); - if (manager != null) - { - manager.AddOrUpdate("ปรับปรุงรอบการลงเวลาทำงาน", Job.FromExpression(x => x.UpdateUserDutyTime()), Cron.Daily(1, 0), TimeZoneInfo.Local); - } - - // apply migrations - await using var scope = app.Services.CreateAsyncScope(); - await using var db = scope.ServiceProvider.GetRequiredService(); - await db.Database.MigrateAsync(); - - // seed default data - await LeaveSeeder.SeedLeaveType(app); - - app.Run(); } +app.MapHealthChecks("/health"); + + +app.UseHttpsRedirection(); +app.UseCors(); +app.UseAuthentication(); +app.UseAuthorization(); +app.UseDefaultFiles(); +app.UseStaticFiles(); +app.MapControllers(); +app.UseMiddleware(); + + +app.UseHangfireDashboard("/hangfire", new DashboardOptions() +{ + Authorization = new[] { new CustomAuthorizeFilter() } +}); + +var manager = new RecurringJobManager(); +if (manager != null) +{ + manager.AddOrUpdate("ปรับปรุงรอบการลงเวลาทำงาน", Job.FromExpression(x => x.UpdateUserDutyTime()), "0 1 * * *", bangkokTimeZone); +} + +// apply migrations +await using var scope = app.Services.CreateAsyncScope(); +await using var db = scope.ServiceProvider.GetRequiredService(); +await db.Database.MigrateAsync(); + +// seed default data +await LeaveSeeder.SeedLeaveType(app); + +app.Run(); + + void ConfigureLogs() { var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");