mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-05 02:32:12 +00:00
Add calendar/events backend API (#8)
* Align EntityFrameworkCore.SqlServer and Tools to 8.0.8
* Add calendar/events backend API
Cherry-picked from main-backup (19994ef); scratch notes file removed.
* Require authentication on CalendarController
Security review found [Authorize] commented out, leaving all 6 calendar
endpoints anonymous. Enforce auth to match the API convention (17/23
controllers).
* Add CalendarController unit tests (xUnit + EF InMemory)
This commit is contained in:
parent
dd1ffba06d
commit
1f3972ae49
10 changed files with 677 additions and 3 deletions
|
|
@ -0,0 +1,25 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
<PackageReference Include="Moq" Version="4.20.72" />
|
||||||
|
<PackageReference Include="FluentAssertions" Version="6.12.1" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.8" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
<ProjectReference Include="..\Api.SeaHavenIndustries\Api.SeaHavenIndustries.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
206
Api.SeaHavenIndustries.Tests/CalendarControllerTests.cs
Normal file
206
Api.SeaHavenIndustries.Tests/CalendarControllerTests.cs
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
using System.Collections;
|
||||||
|
using Api.SeaHavenIndustries.Controllers;
|
||||||
|
using Api.SeaHavenIndustries.DTOs;
|
||||||
|
using Data.SeaHavenIndustries;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Api.SeaHavenIndustries.Tests;
|
||||||
|
|
||||||
|
public class CalendarControllerTests
|
||||||
|
{
|
||||||
|
private static ApplicationDbContext NewContext()
|
||||||
|
{
|
||||||
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
||||||
|
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||||
|
.Options;
|
||||||
|
return new ApplicationDbContext(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UserManager<ApplicationUser> MockUserManager()
|
||||||
|
{
|
||||||
|
var store = new Mock<IUserStore<ApplicationUser>>();
|
||||||
|
return new Mock<UserManager<ApplicationUser>>(
|
||||||
|
store.Object, null!, null!, null!, null!, null!, null!, null!, null!).Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CalendarController NewController(ApplicationDbContext ctx) =>
|
||||||
|
new(MockUserManager(), ctx);
|
||||||
|
|
||||||
|
private static EditEvent_DTO ValidDto(int? id = null, string title = "Standup") => new()
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
Title = title,
|
||||||
|
ReminderMinutes = 15,
|
||||||
|
EventColor = "primary",
|
||||||
|
StartDate = new DateTime(2026, 6, 1),
|
||||||
|
StartTime = "08:00 AM",
|
||||||
|
EndDate = new DateTime(2026, 6, 1),
|
||||||
|
EndTime = "08:30 AM"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static Events SeedEvent(ApplicationDbContext ctx, string title, DateTime startDate, bool? isDeleted = false)
|
||||||
|
{
|
||||||
|
var ev = new Events
|
||||||
|
{
|
||||||
|
Title = title,
|
||||||
|
EventColor = "primary",
|
||||||
|
ReminderMinutes = 15,
|
||||||
|
StartDate = startDate,
|
||||||
|
EndDate = startDate,
|
||||||
|
IsDeleted = isDeleted,
|
||||||
|
CreatedDate = DateTime.Now
|
||||||
|
};
|
||||||
|
ctx.Events.Add(ev);
|
||||||
|
ctx.SaveChanges();
|
||||||
|
return ev;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object Prop(object source, string name) =>
|
||||||
|
source.GetType().GetProperty(name)!.GetValue(source)!;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AddEvent_WhenValid_PersistsEventAndReturnsOk()
|
||||||
|
{
|
||||||
|
using var ctx = NewContext();
|
||||||
|
var result = await NewController(ctx).AddEvent(ValidDto());
|
||||||
|
|
||||||
|
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
|
||||||
|
ok.Value.Should().BeOfType<DataResponse>().Which.Status.Should().Be("200");
|
||||||
|
|
||||||
|
var saved = ctx.Events.Single();
|
||||||
|
saved.Title.Should().Be("Standup");
|
||||||
|
saved.IsDeleted.Should().Be(false);
|
||||||
|
saved.CreatedDate.Should().NotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task EditEvent_WhenIdMissing_ReturnsBadRequest()
|
||||||
|
{
|
||||||
|
using var ctx = NewContext();
|
||||||
|
var result = await NewController(ctx).EditEvent(ValidDto(id: null));
|
||||||
|
|
||||||
|
result.Should().BeOfType<BadRequestObjectResult>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task EditEvent_WhenEventNotFound_ReturnsNotFound()
|
||||||
|
{
|
||||||
|
using var ctx = NewContext();
|
||||||
|
var result = await NewController(ctx).EditEvent(ValidDto(id: 999));
|
||||||
|
|
||||||
|
result.Should().BeOfType<NotFoundObjectResult>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task EditEvent_WhenEventExists_UpdatesFieldsAndStampsModified()
|
||||||
|
{
|
||||||
|
using var ctx = NewContext();
|
||||||
|
var existing = SeedEvent(ctx, "Old", new DateTime(2026, 6, 1));
|
||||||
|
|
||||||
|
var result = await NewController(ctx).EditEvent(ValidDto(id: existing.Id, title: "New"));
|
||||||
|
|
||||||
|
result.Should().BeOfType<OkObjectResult>();
|
||||||
|
var updated = ctx.Events.Single();
|
||||||
|
updated.Title.Should().Be("New");
|
||||||
|
updated.LastModificationTime.Should().NotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetEventById_WhenExists_ReturnsEventDto()
|
||||||
|
{
|
||||||
|
using var ctx = NewContext();
|
||||||
|
var existing = SeedEvent(ctx, "Review", new DateTime(2026, 6, 1));
|
||||||
|
|
||||||
|
var result = await NewController(ctx).GetEventById(existing.Id);
|
||||||
|
|
||||||
|
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
|
||||||
|
var dto = ok.Value.Should().BeOfType<DataResponse>().Subject.Data as Event_DTO;
|
||||||
|
dto!.Title.Should().Be("Review");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetEventById_WhenSoftDeleted_ReturnsNotFound()
|
||||||
|
{
|
||||||
|
using var ctx = NewContext();
|
||||||
|
var deleted = SeedEvent(ctx, "Gone", new DateTime(2026, 6, 1), isDeleted: true);
|
||||||
|
|
||||||
|
var result = await NewController(ctx).GetEventById(deleted.Id);
|
||||||
|
|
||||||
|
result.Should().BeOfType<NotFoundObjectResult>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeleteEvent_WhenExists_SoftDeletesAndKeepsRow()
|
||||||
|
{
|
||||||
|
using var ctx = NewContext();
|
||||||
|
var existing = SeedEvent(ctx, "Cancel", new DateTime(2026, 6, 1));
|
||||||
|
|
||||||
|
var result = await NewController(ctx).DeleteEvent(existing.Id);
|
||||||
|
|
||||||
|
result.Should().BeOfType<OkObjectResult>();
|
||||||
|
var row = ctx.Events.Single();
|
||||||
|
row.IsDeleted.Should().Be(true);
|
||||||
|
row.DeletionTime.Should().NotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeleteEvent_WhenNotFound_ReturnsNotFound()
|
||||||
|
{
|
||||||
|
using var ctx = NewContext();
|
||||||
|
var result = await NewController(ctx).DeleteEvent(12345);
|
||||||
|
|
||||||
|
result.Should().BeOfType<NotFoundObjectResult>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetEventList_ExcludesDeletedAndCountsAfterFilter()
|
||||||
|
{
|
||||||
|
using var ctx = NewContext();
|
||||||
|
SeedEvent(ctx, "Alpha meeting", new DateTime(2026, 6, 1));
|
||||||
|
SeedEvent(ctx, "Beta meeting", new DateTime(2026, 6, 2));
|
||||||
|
SeedEvent(ctx, "Alpha review", new DateTime(2026, 6, 3));
|
||||||
|
SeedEvent(ctx, "Alpha archived", new DateTime(2026, 6, 4), isDeleted: true);
|
||||||
|
|
||||||
|
var result = NewController(ctx).GetEventList(search: "alpha", page: 1, pageSize: 10);
|
||||||
|
|
||||||
|
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
|
||||||
|
var data = ok.Value.Should().BeOfType<DataResponse>().Subject.Data!;
|
||||||
|
((int)Prop(data, "totalCount")).Should().Be(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetEventList_OrdersByStartDateDescending()
|
||||||
|
{
|
||||||
|
using var ctx = NewContext();
|
||||||
|
SeedEvent(ctx, "Earliest", new DateTime(2026, 6, 1));
|
||||||
|
SeedEvent(ctx, "Latest", new DateTime(2026, 6, 10));
|
||||||
|
SeedEvent(ctx, "Middle", new DateTime(2026, 6, 5));
|
||||||
|
|
||||||
|
var result = NewController(ctx).GetEventList(page: 1, pageSize: 10);
|
||||||
|
|
||||||
|
var data = (result as OkObjectResult)!.Value.As<DataResponse>().Data!;
|
||||||
|
var rows = ((IEnumerable)Prop(data, "rows")).Cast<Event_DTO>().ToList();
|
||||||
|
rows.Select(r => r.Title).Should().ContainInOrder("Latest", "Middle", "Earliest");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetEventsForCalendar_ReturnsOnlyEventsOverlappingRange()
|
||||||
|
{
|
||||||
|
using var ctx = NewContext();
|
||||||
|
SeedEvent(ctx, "InRange", new DateTime(2026, 6, 15));
|
||||||
|
SeedEvent(ctx, "BeforeRange", new DateTime(2026, 5, 1));
|
||||||
|
SeedEvent(ctx, "AfterRange", new DateTime(2026, 7, 1));
|
||||||
|
|
||||||
|
var result = NewController(ctx).GetEventsForCalendar(
|
||||||
|
start: new DateTime(2026, 6, 1), end: new DateTime(2026, 6, 30));
|
||||||
|
|
||||||
|
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
|
||||||
|
var events = ok.Value.As<DataResponse>().Data as IEnumerable<Event_DTO>;
|
||||||
|
events!.Select(e => e.Title).Should().BeEquivalentTo(new[] { "InRange" });
|
||||||
|
}
|
||||||
|
}
|
||||||
306
Api.SeaHavenIndustries/Controllers/CalendarController.cs
Normal file
306
Api.SeaHavenIndustries/Controllers/CalendarController.cs
Normal file
|
|
@ -0,0 +1,306 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Api.SeaHavenIndustries.DTOs;
|
||||||
|
using Data.SeaHavenIndustries;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Api.SeaHavenIndustries.Controllers
|
||||||
|
{
|
||||||
|
[Authorize]
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/Calendar")]
|
||||||
|
public class CalendarController : Controller
|
||||||
|
{
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
private readonly ApplicationDbContext _db;
|
||||||
|
|
||||||
|
public CalendarController(UserManager<ApplicationUser> userManager, ApplicationDbContext db)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
_db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================================================
|
||||||
|
// CREATE EVENT
|
||||||
|
// ===================================================================================
|
||||||
|
[HttpPost]
|
||||||
|
[Route("AddEvent")]
|
||||||
|
public async Task<IActionResult> AddEvent(EditEvent_DTO eventDto)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var model = new Events
|
||||||
|
{
|
||||||
|
Title = eventDto.Title,
|
||||||
|
Description = eventDto.Description,
|
||||||
|
Location = eventDto.Location,
|
||||||
|
RecordID = eventDto.RecordID,
|
||||||
|
RecordName = eventDto.RecordName,
|
||||||
|
RecordTargetType = eventDto.RecordTargetType,
|
||||||
|
ReminderMinutes = eventDto.ReminderMinutes,
|
||||||
|
EventColor = eventDto.EventColor,
|
||||||
|
StartDate = eventDto.StartDate,
|
||||||
|
StartTime = eventDto.StartTime,
|
||||||
|
EndDate = eventDto.EndDate,
|
||||||
|
EndTime = eventDto.EndTime,
|
||||||
|
AllDay = eventDto.AllDay,
|
||||||
|
CreatedDate = DateTime.Now,
|
||||||
|
IsDeleted = false
|
||||||
|
};
|
||||||
|
|
||||||
|
_db.Events.Add(model);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Ok(new DataResponse { Message = "Event Created Successfully", Status = "200" });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================================================
|
||||||
|
// UPDATE EVENT
|
||||||
|
// ===================================================================================
|
||||||
|
[HttpPost]
|
||||||
|
[Route("EditEvent")]
|
||||||
|
public async Task<IActionResult> EditEvent(EditEvent_DTO eventDto)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!eventDto.Id.HasValue)
|
||||||
|
{
|
||||||
|
return BadRequest(new Response { Status = "Error", Message = "Event ID is required for editing" });
|
||||||
|
}
|
||||||
|
|
||||||
|
var model = await _db.Events.FindAsync(eventDto.Id.Value);
|
||||||
|
if (model == null || model.IsDeleted == true)
|
||||||
|
{
|
||||||
|
return NotFound(new Response { Status = "Error", Message = "Event not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
model.Title = eventDto.Title;
|
||||||
|
model.Description = eventDto.Description;
|
||||||
|
model.Location = eventDto.Location;
|
||||||
|
model.RecordID = eventDto.RecordID;
|
||||||
|
model.RecordName = eventDto.RecordName;
|
||||||
|
model.RecordTargetType = eventDto.RecordTargetType;
|
||||||
|
model.ReminderMinutes = eventDto.ReminderMinutes;
|
||||||
|
model.EventColor = eventDto.EventColor;
|
||||||
|
model.StartDate = eventDto.StartDate;
|
||||||
|
model.StartTime = eventDto.StartTime;
|
||||||
|
model.EndDate = eventDto.EndDate;
|
||||||
|
model.EndTime = eventDto.EndTime;
|
||||||
|
model.AllDay = eventDto.AllDay;
|
||||||
|
model.LastModificationTime = DateTime.Now;
|
||||||
|
|
||||||
|
_db.Events.Update(model);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Ok(new DataResponse { Message = "Event Updated Successfully", Status = "200" });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================================================
|
||||||
|
// GET EVENT BY ID
|
||||||
|
// ===================================================================================
|
||||||
|
[HttpGet]
|
||||||
|
[Route("GetEventById/{id}")]
|
||||||
|
public async Task<IActionResult> GetEventById(int id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var model = await _db.Events
|
||||||
|
.Where(e => e.Id == id && e.IsDeleted != true)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
if (model == null)
|
||||||
|
{
|
||||||
|
return NotFound(new Response { Status = "Error", Message = "Event not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
var dto = new Event_DTO
|
||||||
|
{
|
||||||
|
Id = model.Id,
|
||||||
|
Title = model.Title,
|
||||||
|
Description = model.Description,
|
||||||
|
Location = model.Location,
|
||||||
|
RecordID = model.RecordID,
|
||||||
|
RecordName = model.RecordName,
|
||||||
|
RecordTargetType = model.RecordTargetType,
|
||||||
|
ReminderMinutes = model.ReminderMinutes,
|
||||||
|
EventColor = model.EventColor,
|
||||||
|
StartDate = model.StartDate,
|
||||||
|
StartTime = model.StartTime,
|
||||||
|
EndDate = model.EndDate,
|
||||||
|
EndTime = model.EndTime,
|
||||||
|
AllDay = model.AllDay,
|
||||||
|
CreatedDate = model.CreatedDate,
|
||||||
|
createdby = model.createdby
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(new DataResponse { Message = "Success", Status = "200", Data = dto });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================================================
|
||||||
|
// DELETE EVENT
|
||||||
|
// ===================================================================================
|
||||||
|
[HttpDelete]
|
||||||
|
[Route("DeleteEvent/{id}")]
|
||||||
|
public async Task<IActionResult> DeleteEvent(int id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var model = await _db.Events.FindAsync(id);
|
||||||
|
if (model == null)
|
||||||
|
{
|
||||||
|
return NotFound(new Response { Status = "Error", Message = "Event not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Soft delete
|
||||||
|
model.IsDeleted = true;
|
||||||
|
model.DeletionTime = DateTime.Now;
|
||||||
|
|
||||||
|
_db.Events.Update(model);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Ok(new DataResponse { Message = "Event Deleted Successfully", Status = "200" });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================================================
|
||||||
|
// GET EVENTS LIST (WITH PAGINATION AND FILTERS)
|
||||||
|
// ===================================================================================
|
||||||
|
[HttpGet]
|
||||||
|
[Route("GetEventList")]
|
||||||
|
public IActionResult GetEventList(string? search = "", int page = 1, int pageSize = 10)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var query = _db.Events.Where(e => e.IsDeleted != true);
|
||||||
|
|
||||||
|
// Apply search filter if provided
|
||||||
|
if (!string.IsNullOrWhiteSpace(search))
|
||||||
|
{
|
||||||
|
var searchLower = search.ToLower();
|
||||||
|
query = query.Where(e =>
|
||||||
|
e.Title.ToLower().Contains(searchLower) ||
|
||||||
|
(e.Description != null && e.Description.ToLower().Contains(searchLower)) ||
|
||||||
|
(e.Location != null && e.Location.ToLower().Contains(searchLower))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// IMPORTANT: Calculate totalCount AFTER applying filters
|
||||||
|
var totalCount = query.Count();
|
||||||
|
|
||||||
|
// Apply pagination
|
||||||
|
var events = query
|
||||||
|
.OrderByDescending(e => e.StartDate)
|
||||||
|
.ThenByDescending(e => e.CreatedDate)
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.Select(e => new Event_DTO
|
||||||
|
{
|
||||||
|
Id = e.Id,
|
||||||
|
Title = e.Title,
|
||||||
|
Description = e.Description,
|
||||||
|
Location = e.Location,
|
||||||
|
RecordID = e.RecordID,
|
||||||
|
RecordName = e.RecordName,
|
||||||
|
RecordTargetType = e.RecordTargetType,
|
||||||
|
ReminderMinutes = e.ReminderMinutes,
|
||||||
|
EventColor = e.EventColor,
|
||||||
|
StartDate = e.StartDate,
|
||||||
|
StartTime = e.StartTime,
|
||||||
|
EndDate = e.EndDate,
|
||||||
|
EndTime = e.EndTime,
|
||||||
|
AllDay = e.AllDay,
|
||||||
|
CreatedDate = e.CreatedDate,
|
||||||
|
createdby = e.createdby
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return Ok(new DataResponse
|
||||||
|
{
|
||||||
|
Message = "Success",
|
||||||
|
Status = "200",
|
||||||
|
Data = new { rows = events, totalCount }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================================================
|
||||||
|
// GET EVENTS FOR CALENDAR (Date Range Filter)
|
||||||
|
// ===================================================================================
|
||||||
|
[HttpGet]
|
||||||
|
[Route("GetEventsForCalendar")]
|
||||||
|
public IActionResult GetEventsForCalendar([FromQuery] DateTime? start, [FromQuery] DateTime? end)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var query = _db.Events.Where(e => e.IsDeleted != true);
|
||||||
|
|
||||||
|
// Filter by date range if provided
|
||||||
|
if (start.HasValue && end.HasValue)
|
||||||
|
{
|
||||||
|
query = query.Where(e =>
|
||||||
|
(e.StartDate >= start.Value && e.StartDate <= end.Value) ||
|
||||||
|
(e.EndDate >= start.Value && e.EndDate <= end.Value) ||
|
||||||
|
(e.StartDate <= start.Value && e.EndDate >= end.Value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var events = query
|
||||||
|
.Select(e => new Event_DTO
|
||||||
|
{
|
||||||
|
Id = e.Id,
|
||||||
|
Title = e.Title,
|
||||||
|
Description = e.Description,
|
||||||
|
Location = e.Location,
|
||||||
|
RecordID = e.RecordID,
|
||||||
|
RecordName = e.RecordName,
|
||||||
|
RecordTargetType = e.RecordTargetType,
|
||||||
|
ReminderMinutes = e.ReminderMinutes,
|
||||||
|
EventColor = e.EventColor,
|
||||||
|
StartDate = e.StartDate,
|
||||||
|
StartTime = e.StartTime,
|
||||||
|
EndDate = e.EndDate,
|
||||||
|
EndTime = e.EndTime,
|
||||||
|
AllDay = e.AllDay,
|
||||||
|
CreatedDate = e.CreatedDate,
|
||||||
|
createdby = e.createdby
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return Ok(new DataResponse { Message = "Success", Status = "200", Data = events });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
48
Api.SeaHavenIndustries/DTOs/EditEvent_DTO.cs
Normal file
48
Api.SeaHavenIndustries/DTOs/EditEvent_DTO.cs
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Api.SeaHavenIndustries.DTOs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// DTO for POST/PUT requests - Create/Edit Event
|
||||||
|
/// Used to create or update event information
|
||||||
|
/// </summary>
|
||||||
|
public class EditEvent_DTO
|
||||||
|
{
|
||||||
|
public int? Id { get; set; } // Null for create, populated for edit
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "Please enter a title")]
|
||||||
|
[StringLength(100, ErrorMessage = "Title cannot be longer than 100 characters")]
|
||||||
|
public string Title { get; set; } = null!;
|
||||||
|
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
[StringLength(150, ErrorMessage = "Location cannot be longer than 150 characters")]
|
||||||
|
public string? Location { get; set; }
|
||||||
|
|
||||||
|
public int? RecordID { get; set; }
|
||||||
|
public string? RecordName { get; set; }
|
||||||
|
public string? RecordTargetType { get; set; }
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "The Remind Attendees field is required")]
|
||||||
|
public int ReminderMinutes { get; set; } = 15;
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "The Select a Calendar Color field is required")]
|
||||||
|
public string EventColor { get; set; } = "primary";
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "Please select a start date")]
|
||||||
|
public DateTime StartDate { get; set; }
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "Please select a start time")]
|
||||||
|
public string StartTime { get; set; } = null!;
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "Please select an end date")]
|
||||||
|
public DateTime EndDate { get; set; }
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "Please select an end time")]
|
||||||
|
public string EndTime { get; set; } = null!;
|
||||||
|
|
||||||
|
public bool AllDay { get; set; } = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
33
Api.SeaHavenIndustries/DTOs/Event_DTO.cs
Normal file
33
Api.SeaHavenIndustries/DTOs/Event_DTO.cs
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Api.SeaHavenIndustries.DTOs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// DTO for GET responses - Event data
|
||||||
|
/// Used to return event information to the client
|
||||||
|
/// </summary>
|
||||||
|
public class Event_DTO
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Title { get; set; } = null!;
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public string? Location { get; set; }
|
||||||
|
|
||||||
|
public int? RecordID { get; set; }
|
||||||
|
public string? RecordName { get; set; }
|
||||||
|
public string? RecordTargetType { get; set; }
|
||||||
|
|
||||||
|
public int ReminderMinutes { get; set; }
|
||||||
|
public string EventColor { get; set; } = null!;
|
||||||
|
|
||||||
|
public DateTime StartDate { get; set; }
|
||||||
|
public string? StartTime { get; set; }
|
||||||
|
public DateTime EndDate { get; set; }
|
||||||
|
public string? EndTime { get; set; }
|
||||||
|
public bool AllDay { get; set; }
|
||||||
|
|
||||||
|
public DateTime? CreatedDate { get; set; }
|
||||||
|
public string? createdby { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -66,6 +66,7 @@ namespace Data.SeaHavenIndustries
|
||||||
public DbSet<FollowUps> FollowUps { get; set; }
|
public DbSet<FollowUps> FollowUps { get; set; }
|
||||||
public DbSet<PMSchedules> PMSchedules { get; set; }
|
public DbSet<PMSchedules> PMSchedules { get; set; }
|
||||||
public DbSet<ContactDetails> ContactDetails { get; set; }
|
public DbSet<ContactDetails> ContactDetails { get; set; }
|
||||||
|
public DbSet<Events> Events { get; set; }
|
||||||
|
|
||||||
// Employee related DbSets
|
// Employee related DbSets
|
||||||
public DbSet<EmployeeAddress> EmployeeAddresses { get; set; }
|
public DbSet<EmployeeAddress> EmployeeAddresses { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.8" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.8" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.4" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
49
Data.SeaHavenIndustries/Models/Events.cs
Normal file
49
Data.SeaHavenIndustries/Models/Events.cs
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Data.SeaHavenIndustries
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Events entity for Calendar/Events module
|
||||||
|
/// Tracks calendar events, appointments, meetings, etc.
|
||||||
|
/// </summary>
|
||||||
|
public class Events : FullAuditEntity
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
[StringLength(100)]
|
||||||
|
public string Title { get; set; } = null!;
|
||||||
|
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
[StringLength(150)]
|
||||||
|
public string? Location { get; set; }
|
||||||
|
|
||||||
|
// Related Record (can link to Accounts, Contacts, Work Orders, etc.)
|
||||||
|
public int? RecordID { get; set; }
|
||||||
|
public string? RecordName { get; set; }
|
||||||
|
public string? RecordTargetType { get; set; }
|
||||||
|
|
||||||
|
// Reminder Settings
|
||||||
|
public int ReminderMinutes { get; set; } = 15; // Default: 15 minutes before
|
||||||
|
|
||||||
|
// Color for calendar display
|
||||||
|
[StringLength(20)]
|
||||||
|
public string EventColor { get; set; } = "primary"; // primary, success, info, warning, danger
|
||||||
|
|
||||||
|
// Schedule
|
||||||
|
[Required]
|
||||||
|
public DateTime StartDate { get; set; }
|
||||||
|
|
||||||
|
[StringLength(10)]
|
||||||
|
public string? StartTime { get; set; } // e.g., "08:00 AM"
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public DateTime EndDate { get; set; }
|
||||||
|
|
||||||
|
[StringLength(10)]
|
||||||
|
public string? EndTime { get; set; } // e.g., "08:30 AM"
|
||||||
|
|
||||||
|
public bool AllDay { get; set; } = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeaHaven.Services", "SeaHav
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeaHaven.DataServices", "SeaHaven.DataServices\SeaHaven.DataServices.csproj", "{60DF3943-9BC3-4444-8E7A-EEC34DDC7806}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeaHaven.DataServices", "SeaHaven.DataServices\SeaHaven.DataServices.csproj", "{60DF3943-9BC3-4444-8E7A-EEC34DDC7806}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api.SeaHavenIndustries.Tests", "Api.SeaHavenIndustries.Tests\Api.SeaHavenIndustries.Tests.csproj", "{1222E2E5-2FB0-4AA8-842C-0D77CFBA5E45}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
|
@ -39,6 +41,10 @@ Global
|
||||||
{60DF3943-9BC3-4444-8E7A-EEC34DDC7806}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{60DF3943-9BC3-4444-8E7A-EEC34DDC7806}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{60DF3943-9BC3-4444-8E7A-EEC34DDC7806}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{60DF3943-9BC3-4444-8E7A-EEC34DDC7806}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{60DF3943-9BC3-4444-8E7A-EEC34DDC7806}.Release|Any CPU.Build.0 = Release|Any CPU
|
{60DF3943-9BC3-4444-8E7A-EEC34DDC7806}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{1222E2E5-2FB0-4AA8-842C-0D77CFBA5E45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{1222E2E5-2FB0-4AA8-842C-0D77CFBA5E45}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{1222E2E5-2FB0-4AA8-842C-0D77CFBA5E45}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{1222E2E5-2FB0-4AA8-842C-0D77CFBA5E45}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@
|
||||||
<PackageReference Include="BlazorGoogleMaps" Version="4.7.7" />
|
<PackageReference Include="BlazorGoogleMaps" Version="4.7.7" />
|
||||||
<PackageReference Include="EPPlus" Version="7.3.0" />
|
<PackageReference Include="EPPlus" Version="7.3.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.4" />
|
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.4" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.4" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.4" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.8" />
|
||||||
<PackageReference Include="PasswordGenerator" Version="2.1.0" />
|
<PackageReference Include="PasswordGenerator" Version="2.1.0" />
|
||||||
<PackageReference Include="PuppeteerSharp" Version="19.0.1" />
|
<PackageReference Include="PuppeteerSharp" Version="19.0.1" />
|
||||||
<PackageReference Include="Radzen.Blazor" Version="5.1.3" />
|
<PackageReference Include="Radzen.Blazor" Version="5.1.3" />
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue