From 1f3972ae49425cd2b2c754834744644dbd64c9be Mon Sep 17 00:00:00 2001
From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com>
Date: Mon, 22 Jun 2026 18:46:46 -0400
Subject: [PATCH] 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)
---
.../Api.SeaHavenIndustries.Tests.csproj | 25 ++
.../CalendarControllerTests.cs | 206 ++++++++++++
.../Controllers/CalendarController.cs | 306 ++++++++++++++++++
Api.SeaHavenIndustries/DTOs/EditEvent_DTO.cs | 48 +++
Api.SeaHavenIndustries/DTOs/Event_DTO.cs | 33 ++
.../Auth/ApplicationDbContext.cs | 1 +
.../Data.SeaHavenIndustries.csproj | 2 +-
Data.SeaHavenIndustries/Models/Events.cs | 49 +++
SeaHavenIndustries.sln | 6 +
SeaHavenIndustries/SeaHavenIndustries.csproj | 4 +-
10 files changed, 677 insertions(+), 3 deletions(-)
create mode 100644 Api.SeaHavenIndustries.Tests/Api.SeaHavenIndustries.Tests.csproj
create mode 100644 Api.SeaHavenIndustries.Tests/CalendarControllerTests.cs
create mode 100644 Api.SeaHavenIndustries/Controllers/CalendarController.cs
create mode 100644 Api.SeaHavenIndustries/DTOs/EditEvent_DTO.cs
create mode 100644 Api.SeaHavenIndustries/DTOs/Event_DTO.cs
create mode 100644 Data.SeaHavenIndustries/Models/Events.cs
diff --git a/Api.SeaHavenIndustries.Tests/Api.SeaHavenIndustries.Tests.csproj b/Api.SeaHavenIndustries.Tests/Api.SeaHavenIndustries.Tests.csproj
new file mode 100644
index 0000000..01e908a
--- /dev/null
+++ b/Api.SeaHavenIndustries.Tests/Api.SeaHavenIndustries.Tests.csproj
@@ -0,0 +1,25 @@
+
+
+
+ net8.0
+ enable
+ enable
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Api.SeaHavenIndustries.Tests/CalendarControllerTests.cs b/Api.SeaHavenIndustries.Tests/CalendarControllerTests.cs
new file mode 100644
index 0000000..571b606
--- /dev/null
+++ b/Api.SeaHavenIndustries.Tests/CalendarControllerTests.cs
@@ -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()
+ .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
+ .Options;
+ return new ApplicationDbContext(options);
+ }
+
+ private static UserManager MockUserManager()
+ {
+ var store = new Mock>();
+ return new Mock>(
+ 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().Subject;
+ ok.Value.Should().BeOfType().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();
+ }
+
+ [Fact]
+ public async Task EditEvent_WhenEventNotFound_ReturnsNotFound()
+ {
+ using var ctx = NewContext();
+ var result = await NewController(ctx).EditEvent(ValidDto(id: 999));
+
+ result.Should().BeOfType();
+ }
+
+ [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();
+ 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().Subject;
+ var dto = ok.Value.Should().BeOfType().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();
+ }
+
+ [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();
+ 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();
+ }
+
+ [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().Subject;
+ var data = ok.Value.Should().BeOfType().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().Data!;
+ var rows = ((IEnumerable)Prop(data, "rows")).Cast().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().Subject;
+ var events = ok.Value.As().Data as IEnumerable;
+ events!.Select(e => e.Title).Should().BeEquivalentTo(new[] { "InRange" });
+ }
+}
diff --git a/Api.SeaHavenIndustries/Controllers/CalendarController.cs b/Api.SeaHavenIndustries/Controllers/CalendarController.cs
new file mode 100644
index 0000000..2d7f85d
--- /dev/null
+++ b/Api.SeaHavenIndustries/Controllers/CalendarController.cs
@@ -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 _userManager;
+ private readonly ApplicationDbContext _db;
+
+ public CalendarController(UserManager userManager, ApplicationDbContext db)
+ {
+ _userManager = userManager;
+ _db = db;
+ }
+
+ // ===================================================================================
+ // CREATE EVENT
+ // ===================================================================================
+ [HttpPost]
+ [Route("AddEvent")]
+ public async Task 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 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 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 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 });
+ }
+ }
+ }
+}
+
diff --git a/Api.SeaHavenIndustries/DTOs/EditEvent_DTO.cs b/Api.SeaHavenIndustries/DTOs/EditEvent_DTO.cs
new file mode 100644
index 0000000..0083de5
--- /dev/null
+++ b/Api.SeaHavenIndustries/DTOs/EditEvent_DTO.cs
@@ -0,0 +1,48 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+
+namespace Api.SeaHavenIndustries.DTOs
+{
+ ///
+ /// DTO for POST/PUT requests - Create/Edit Event
+ /// Used to create or update event information
+ ///
+ 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;
+ }
+}
+
diff --git a/Api.SeaHavenIndustries/DTOs/Event_DTO.cs b/Api.SeaHavenIndustries/DTOs/Event_DTO.cs
new file mode 100644
index 0000000..2543831
--- /dev/null
+++ b/Api.SeaHavenIndustries/DTOs/Event_DTO.cs
@@ -0,0 +1,33 @@
+using System;
+
+namespace Api.SeaHavenIndustries.DTOs
+{
+ ///
+ /// DTO for GET responses - Event data
+ /// Used to return event information to the client
+ ///
+ 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; }
+ }
+}
+
diff --git a/Data.SeaHavenIndustries/Auth/ApplicationDbContext.cs b/Data.SeaHavenIndustries/Auth/ApplicationDbContext.cs
index d66f708..ea60e8e 100644
--- a/Data.SeaHavenIndustries/Auth/ApplicationDbContext.cs
+++ b/Data.SeaHavenIndustries/Auth/ApplicationDbContext.cs
@@ -66,6 +66,7 @@ namespace Data.SeaHavenIndustries
public DbSet FollowUps { get; set; }
public DbSet PMSchedules { get; set; }
public DbSet ContactDetails { get; set; }
+ public DbSet Events { get; set; }
// Employee related DbSets
public DbSet EmployeeAddresses { get; set; }
diff --git a/Data.SeaHavenIndustries/Data.SeaHavenIndustries.csproj b/Data.SeaHavenIndustries/Data.SeaHavenIndustries.csproj
index 183423b..4ee8133 100644
--- a/Data.SeaHavenIndustries/Data.SeaHavenIndustries.csproj
+++ b/Data.SeaHavenIndustries/Data.SeaHavenIndustries.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/Data.SeaHavenIndustries/Models/Events.cs b/Data.SeaHavenIndustries/Models/Events.cs
new file mode 100644
index 0000000..97ed818
--- /dev/null
+++ b/Data.SeaHavenIndustries/Models/Events.cs
@@ -0,0 +1,49 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+
+namespace Data.SeaHavenIndustries
+{
+ ///
+ /// Events entity for Calendar/Events module
+ /// Tracks calendar events, appointments, meetings, etc.
+ ///
+ 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;
+ }
+}
+
diff --git a/SeaHavenIndustries.sln b/SeaHavenIndustries.sln
index 5560d76..b9df8e5 100644
--- a/SeaHavenIndustries.sln
+++ b/SeaHavenIndustries.sln
@@ -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
diff --git a/SeaHavenIndustries/SeaHavenIndustries.csproj b/SeaHavenIndustries/SeaHavenIndustries.csproj
index 11de19e..f3b6061 100644
--- a/SeaHavenIndustries/SeaHavenIndustries.csproj
+++ b/SeaHavenIndustries/SeaHavenIndustries.csproj
@@ -13,8 +13,8 @@
-
-
+
+