using System.Globalization; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using PlotLine.Services; namespace PlotLine.Controllers; [Authorize] public sealed class StoryCalendarController(IStoryCalendarService calendar, ILogger logger) : Controller { public async Task Index(int projectId, int? bookId, string? month, string? date) { if (projectId == 0) { return BadRequest("ProjectID is required."); } var selectedMonth = ParseMonth(month); var selectedDate = ParseDate(date); var model = await calendar.GetAsync(projectId, bookId, selectedMonth, selectedDate); if (model is null) { return NotFound(); } return IsAjaxRequest() ? PartialView("_StoryCalendarSurface", model) : View(model); } [HttpGet] public async Task DayDetails(int projectId, int? bookId, string? date) { if (projectId == 0) { return BadRequest("ProjectID is required."); } var selectedDate = ParseDate(date); if (!selectedDate.HasValue) { return BadRequest("Date is required."); } try { var model = await calendar.GetAsync(projectId, bookId, FirstOfMonth(selectedDate.Value), selectedDate); return model is null ? NotFound() : PartialView("_StoryCalendarDayDetails", model.DayDetails); } catch (Exception ex) { logger.LogError(ex, "Failed to load Story Calendar day details for project {ProjectID} and date {StoryDate}.", projectId, date); return StatusCode(StatusCodes.Status500InternalServerError, "The selected story date could not be loaded."); } } [HttpPost] [ValidateAntiForgeryToken] public IActionResult ApplyFilters(int projectId, int? bookId, string? month) { return RedirectToAction(nameof(Index), new { projectId, bookId, month }); } private static DateOnly? ParseMonth(string? value) { if (string.IsNullOrWhiteSpace(value)) { return null; } return DateOnly.TryParseExact( $"{value.Trim()}-01", "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsed) ? new DateOnly(parsed.Year, parsed.Month, 1) : null; } private static DateOnly? ParseDate(string? value) { if (string.IsNullOrWhiteSpace(value)) { return null; } return DateOnly.TryParseExact( value.Trim(), "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsed) ? parsed : null; } private static DateOnly FirstOfMonth(DateOnly date) => new(date.Year, date.Month, 1); private bool IsAjaxRequest() => string.Equals(Request.Headers["X-Requested-With"], "XMLHttpRequest", StringComparison.OrdinalIgnoreCase); }