mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 04:53:11 +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<PMSchedules> PMSchedules { get; set; }
|
||||
public DbSet<ContactDetails> ContactDetails { get; set; }
|
||||
public DbSet<Events> Events { get; set; }
|
||||
|
||||
// Employee related DbSets
|
||||
public DbSet<EmployeeAddress> EmployeeAddresses { get; set; }
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.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>
|
||||
|
||||
</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
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeaHaven.DataServices", "SeaHaven.DataServices\SeaHaven.DataServices.csproj", "{60DF3943-9BC3-4444-8E7A-EEC34DDC7806}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api.SeaHavenIndustries.Tests", "Api.SeaHavenIndustries.Tests\Api.SeaHavenIndustries.Tests.csproj", "{1222E2E5-2FB0-4AA8-842C-0D77CFBA5E45}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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}.Release|Any CPU.ActiveCfg = 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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@
|
|||
<PackageReference Include="BlazorGoogleMaps" Version="4.7.7" />
|
||||
<PackageReference Include="EPPlus" Version="7.3.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.8" />
|
||||
<PackageReference Include="PasswordGenerator" Version="2.1.0" />
|
||||
<PackageReference Include="PuppeteerSharp" Version="19.0.1" />
|
||||
<PackageReference Include="Radzen.Blazor" Version="5.1.3" />
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue