✨ Notable days (holiday)
This commit is contained in:
		
							
								
								
									
										53
									
								
								DysonNetwork.Pass/Account/NotableDay.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								DysonNetwork.Pass/Account/NotableDay.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| using Nager.Holiday; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Account; | ||||
|  | ||||
| /// <summary> | ||||
| /// Reference from Nager.Holiday | ||||
| /// </summary> | ||||
| public enum NotableHolidayType | ||||
| { | ||||
|     /// <summary>Public holiday</summary> | ||||
|     Public, | ||||
|     /// <summary>Bank holiday, banks and offices are closed</summary> | ||||
|     Bank, | ||||
|     /// <summary>School holiday, schools are closed</summary> | ||||
|     School, | ||||
|     /// <summary>Authorities are closed</summary> | ||||
|     Authorities, | ||||
|     /// <summary>Majority of people take a day off</summary> | ||||
|     Optional, | ||||
|     /// <summary>Optional festivity, no paid day off</summary> | ||||
|     Observance, | ||||
| } | ||||
|  | ||||
|  | ||||
| public class NotableDay | ||||
| { | ||||
|     public Instant Date { get; set; } | ||||
|     public string? LocalName { get; set; } | ||||
|     public string? GlobalName { get; set; } | ||||
|     public string? CountryCode { get; set; } | ||||
|     public NotableHolidayType[] Holidays { get; set; } = []; | ||||
|  | ||||
|     public static NotableDay FromNagerHoliday(PublicHoliday holiday) | ||||
|     { | ||||
|         return new NotableDay() | ||||
|         { | ||||
|             Date = Instant.FromDateTimeUtc(holiday.Date.ToUniversalTime()), | ||||
|             LocalName = holiday.LocalName, | ||||
|             GlobalName = holiday.Name, | ||||
|             CountryCode = holiday.CountryCode, | ||||
|             Holidays = holiday.Types?.Select(x => x switch | ||||
|             { | ||||
|                 PublicHolidayType.Public => NotableHolidayType.Public, | ||||
|                 PublicHolidayType.Bank => NotableHolidayType.Bank, | ||||
|                 PublicHolidayType.School => NotableHolidayType.School, | ||||
|                 PublicHolidayType.Authorities => NotableHolidayType.Authorities, | ||||
|                 PublicHolidayType.Optional => NotableHolidayType.Optional, | ||||
|                 _ => NotableHolidayType.Observance | ||||
|             }).ToArray() ?? [], | ||||
|         }; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										51
									
								
								DysonNetwork.Pass/Account/NotableDaysController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								DysonNetwork.Pass/Account/NotableDaysController.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Account; | ||||
|  | ||||
| [ApiController] | ||||
| [Route("/api/notable")] | ||||
| public class NotableDaysController(NotableDaysService days) : ControllerBase | ||||
| { | ||||
|     [HttpGet("{regionCode}/{year:int}")] | ||||
|     public async Task<ActionResult<List<NotableDay>>> GetRegionDays(string regionCode, int year) | ||||
|     { | ||||
|         var result = await days.GetNotableDays(year, regionCode); | ||||
|         return Ok(result); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("{regionCode}")] | ||||
|     public async Task<ActionResult<List<NotableDay>>> GetRegionDaysCurrentYear(string regionCode) | ||||
|     { | ||||
|         var currentYear = DateTime.Now.Year; | ||||
|         var result = await days.GetNotableDays(currentYear, regionCode); | ||||
|         return Ok(result); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("me/{year:int}")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<List<NotableDay>>> GetAccountNotableDays(int year) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|  | ||||
|         var region = currentUser.Region; | ||||
|         if (string.IsNullOrWhiteSpace(region)) region = "us"; | ||||
|  | ||||
|         var result = await days.GetNotableDays(year, region); | ||||
|         return Ok(result); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("me")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<List<NotableDay>>> GetAccountNotableDaysCurrentYear() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|  | ||||
|         var currentYear = DateTime.Now.Year; | ||||
|         var region = currentUser.Region; | ||||
|         if (string.IsNullOrWhiteSpace(region)) region = "us"; | ||||
|  | ||||
|         var result = await days.GetNotableDays(currentYear, region); | ||||
|         return Ok(result); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										34
									
								
								DysonNetwork.Pass/Account/NotableDaysService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								DysonNetwork.Pass/Account/NotableDaysService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using Nager.Holiday; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Account; | ||||
|  | ||||
| public class NotableDaysService(ICacheService cache) | ||||
| { | ||||
|     private const string NotableDaysCacheKeyPrefix = "notable:"; | ||||
|  | ||||
|     public async Task<List<NotableDay>> GetNotableDays(int? year, string regionCode) | ||||
|     { | ||||
|         year ??= DateTime.UtcNow.Year; | ||||
|  | ||||
|         // Generate cache key using year and region code | ||||
|         var cacheKey = $"{NotableDaysCacheKeyPrefix}:{year}:{regionCode}"; | ||||
|  | ||||
|         // Try to get from cache first | ||||
|         var (found, cachedDays) = await cache.GetAsyncWithStatus<List<NotableDay>>(cacheKey); | ||||
|         if (found && cachedDays != null) | ||||
|         { | ||||
|             return cachedDays; | ||||
|         } | ||||
|  | ||||
|         // If not in cache, fetch from API | ||||
|         using var holidayClient = new HolidayClient(); | ||||
|         var holidays = await holidayClient.GetHolidaysAsync(year.Value, regionCode); | ||||
|         var days = holidays?.Select(NotableDay.FromNagerHoliday).ToList() ?? []; | ||||
|  | ||||
|         // Cache the result for 1 day (holiday data doesn't change frequently) | ||||
|         await cache.SetAsync(cacheKey, days, TimeSpan.FromDays(1)); | ||||
|  | ||||
|         return days; | ||||
|     } | ||||
| } | ||||
| @@ -193,6 +193,7 @@ public static class ServiceCollectionExtensions | ||||
|         services.AddScoped<ActionLogService>(); | ||||
|         services.AddScoped<AccountService>(); | ||||
|         services.AddScoped<AccountEventService>(); | ||||
|         services.AddScoped<NotableDaysService>(); | ||||
|         services.AddScoped<ActionLogService>(); | ||||
|         services.AddScoped<RelationshipService>(); | ||||
|         services.AddScoped<MagicSpellService>(); | ||||
|   | ||||
| @@ -112,6 +112,8 @@ | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APath_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fe6898c1ddf974e16b95b114722270029e55000_003F5c_003F24270f3e_003FPath_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APresignedGetObjectArgs_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F0df26a9d89e29319e9efcaea0a8489db9e97bc1aedcca3f7e360cc50f8f4ea_003FPresignedGetObjectArgs_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APresignedPutObjectArgs_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F9202b7fc1762440f9948968ca0e7397b62200_003Fc1_003F4de0298e_003FPresignedPutObjectArgs_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APublicHolidayType_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F7256e0468d0b4a9ca42aecf1bfb8a7b12a00_003Fb9_003F806179e4_003FPublicHolidayType_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APublicHoliday_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F7256e0468d0b4a9ca42aecf1bfb8a7b12a00_003Fd3_003Fa7bc1284_003FPublicHoliday_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APutObjectArgs_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F556d6adabb16af913b52d8825c320d848cd36ae3d8818e8d12db3a23680db73_003FPutObjectArgs_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APutObjectArgs_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F6efe388c7585d5dd5587416a55298550b030c2a107edf45f988791297c3ffa_003FPutObjectArgs_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AQueryable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F42d8f09d6a294d00a6f49efc989927492fe00_003F4e_003F26d1ee34_003FQueryable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user