Merge branch 'main' into dev

This commit is contained in:
Adam Moussa 2026-06-22 18:57:13 -04:00
commit 12fd5efe8b
127 changed files with 8454 additions and 1699 deletions

34
.env.example Normal file
View file

@ -0,0 +1,34 @@
# Sea Haven Industries — shoc-backend required configuration
#
# This file documents the secrets that used to be hardcoded in appsettings*.json
# and source files. Copy the values into one of the supported configuration sources;
# do NOT commit real values.
#
# .NET resolves configuration in this order (later wins):
# 1. appsettings.json / appsettings.{Environment}.json (committed — placeholders only)
# 2. User Secrets (local dev): dotnet user-secrets set "Key:Sub" "value"
# 3. Environment variables (use "__" as the section separator)
#
# Environment-variable form is shown below. In AWS Elastic Beanstalk these map to
# environment properties; locally you can export them or use dotnet user-secrets.
#
# AWS credentials for the S3 client are NOT listed here on purpose: UploadFileHp now
# uses the AWS SDK default credential chain (env AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY,
# the shared profile/SSO, or the EC2/ECS instance role). Prefer an instance role in prod.
# --- SQL Server connection string (Api.SeaHavenIndustries + SeaHavenIndustries) ---
ConnectionStrings__DefaultConnection=Server=<host>;Initial Catalog=<db>;User Id=<user>;Password=<password>;MultipleActiveResultSets=true
# --- SendGrid (transactional email) ---
SendGrid__ApiKey=SG.xxxxxxxxxxxxxxxxxxxxxx
# --- JWT signing secret (Api.SeaHavenIndustries) ---
JWT__Secret=<a-long-random-secret>
# --- Google Maps / Places API key (SeaHavenIndustries Blazor app) ---
GoogleMaps__ApiKey=AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
# --- AWS S3 (optional; prefer instance role / SSO over static keys) ---
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
# AWS_REGION=us-east-2

11
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,11 @@
name: CI
on:
pull_request:
branches: [main, dev]
jobs:
ci:
uses: Sea-Haven-Industries/.github/.github/workflows/ci-dotnet.yaml@main
with:
dotnet-version: "8.0.x"
solution: "SeaHavenIndustries.sln"
run-tests: true

View file

@ -0,0 +1,6 @@
name: Dependency Review
on:
pull_request:
jobs:
review:
uses: Sea-Haven-Industries/.github/.github/workflows/callable-dependency-review.yaml@main

11
.github/workflows/labeler.yml vendored Normal file
View file

@ -0,0 +1,11 @@
name: Labeler
on:
pull_request:
branches: [main]
permissions:
contents: read
pull-requests: write
issues: write
jobs:
label:
uses: Sea-Haven-Industries/.github/.github/workflows/callable-labeler.yaml@main

View file

@ -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>

View 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" });
}
}

View file

@ -20,6 +20,8 @@
<ItemGroup>
<ProjectReference Include="..\Data.SeaHavenIndustries\Data.SeaHavenIndustries.csproj" />
<ProjectReference Include="..\SeaHaven.Services\SeaHaven.Services.csproj" />
<ProjectReference Include="..\SeaHaven.DataServices\SeaHaven.DataServices.csproj" />
</ItemGroup>
</Project>

View file

@ -1,18 +1,22 @@
using Api.SeaHavenIndustries.DTOs;
using Data.SeaHavenIndustries;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SeaHaven.Services.Interfaces;
using SeaHaven.Services.DTOs;
using System.Security.Claims;
[ApiController]
[Route("api/Account")]
[Route("api/accounts")]
public class AccountController : ControllerBase
{
private readonly ApplicationDbContext _db;
private readonly IAccountService _accountService;
public AccountController(ApplicationDbContext db)
public AccountController(IAccountService accountService)
{
_db = db;
_accountService = accountService;
}
// -----------------------------------
@ -23,34 +27,19 @@ public class AccountController : ControllerBase
{
try
{
var model = new Accounts
var createDto = new CreateAccountDTO
{
Name = dto.Name,
Type = dto.Type,
Importance = dto.Importance,
TerritoryId = dto.TerritoryId,
OwnerEmployeeId = dto.OwnerEmployeeId,
PrivacySetting = dto.PrivacySetting,
About = dto.About,
Tags = dto.Tags,
Name = dto.Name ?? throw new ArgumentException("Name is required"),
Phone = dto.Phone,
Email = dto.Email,
IndustryId = dto.IndustryId,
TimeZone = dto.TimeZone,
Website = dto.Website,
CurrencyCode = dto.CurrencyCode,
ServiceInstructions = dto.ServiceInstructions,
FacebookUrl = dto.FacebookUrl,
LinkedInUrl = dto.LinkedInUrl,
TwitterUrl = dto.TwitterUrl,
CreatedDate = DateTime.UtcNow
Website = dto.Website
};
_db.Accounts.Add(model);
await _db.SaveChangesAsync();
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
await _accountService.CreateAccountAsync(createDto, userId);
return Ok(new DataResponse
{
@ -58,13 +47,14 @@ public class AccountController : ControllerBase
Status = "200"
});
}
catch (ValidationException vex)
{
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
return BadRequest(new Response { Status = "Validation Error", Message = errors });
}
catch (Exception ex)
{
return BadRequest(new Response
{
Status = "Error",
Message = ex.Message
});
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
@ -76,34 +66,19 @@ public class AccountController : ControllerBase
{
try
{
var account = await _db.Accounts.FirstOrDefaultAsync(a => a.Id == dto.Id);
if (account == null)
return BadRequest(new Response { Status = "Error", Message = "Account not found." });
var updateDto = new UpdateAccountDTO
{
Name = dto.Name,
Phone = dto.Phone,
Email = dto.Email,
Website = dto.Website
};
account.Name = dto.Name;
account.Type = dto.Type;
account.Importance = dto.Importance;
account.TerritoryId = dto.TerritoryId;
account.OwnerEmployeeId = dto.OwnerEmployeeId;
account.PrivacySetting = dto.PrivacySetting;
account.About = dto.About;
account.Tags = dto.Tags;
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
account.Phone = dto.Phone;
account.Email = dto.Email;
account.IndustryId = dto.IndustryId;
account.TimeZone = dto.TimeZone;
account.Website = dto.Website;
account.CurrencyCode = dto.CurrencyCode;
account.ServiceInstructions = dto.ServiceInstructions;
account.FacebookUrl = dto.FacebookUrl;
account.LinkedInUrl = dto.LinkedInUrl;
account.TwitterUrl = dto.TwitterUrl;
account.LastModificationTime = DateTime.UtcNow;
await _db.SaveChangesAsync();
await _accountService.UpdateAccountAsync(dto.Id, updateDto, userId);
return Ok(new DataResponse
{
@ -111,13 +86,18 @@ public class AccountController : ControllerBase
Status = "200"
});
}
catch (ValidationException vex)
{
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
return BadRequest(new Response { Status = "Validation Error", Message = errors });
}
catch (KeyNotFoundException)
{
return NotFound(new Response { Status = "Error", Message = "Account not found" });
}
catch (Exception ex)
{
return BadRequest(new Response
{
Status = "Error",
Message = ex.Message
});
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
@ -129,12 +109,11 @@ public class AccountController : ControllerBase
{
try
{
var account = await _db.Accounts.FirstOrDefaultAsync(a => a.Id == id);
if (account == null)
return BadRequest(new Response { Status = "Error", Message = "Account not found." });
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
_db.Accounts.Remove(account);
await _db.SaveChangesAsync();
await _accountService.DeleteAccountAsync(id, userId);
return Ok(new DataResponse
{
@ -142,13 +121,13 @@ public class AccountController : ControllerBase
Status = "200"
});
}
catch (KeyNotFoundException)
{
return NotFound(new Response { Status = "Error", Message = "Account not found" });
}
catch (Exception ex)
{
return BadRequest(new Response
{
Status = "Error",
Message = ex.Message
});
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
@ -156,49 +135,28 @@ public class AccountController : ControllerBase
// GET: api/Account/GetAccountList?search=&page=&pageSize=
// -----------------------------------
[HttpGet("GetAccountList")]
public IActionResult GetAccountList(string? search = "", int page = 1, int pageSize = 12)
public async Task<IActionResult> GetAccountList(string? search = "", int page = 1, int pageSize = 12)
{
search ??= "";
var pagedResult = await _accountService.GetAccountsPagedAsync(page, pageSize, search);
var query = _db.Accounts
.Where(a => a.IsDeleted == false || a.IsDeleted == null);
if (!string.IsNullOrWhiteSpace(search))
var data = pagedResult.Items.Select(a => new
{
var s = search.ToLower();
query = query.Where(a =>
(a.Name ?? "").ToLower().Contains(s) ||
(a.Email ?? "").ToLower().Contains(s) ||
(a.Phone ?? "").Contains(s) ||
(a.Website ?? "").ToLower().Contains(s)
);
}
var totalCount = query.Count();
var data = query
.OrderByDescending(a => a.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(a => new
{
a.Id,
a.Name,
a.Type,
a.Phone,
a.Email,
Industry = a.IndustryId,
a.Website
})
.ToList();
Id = a.Id,
Name = a.Name,
Type = a.Type ?? "",
Phone = a.Phone,
Email = a.Email,
Industry = (int?)null, // Map if needed
Website = a.Website
});
var viewModel = new Pagination_DTO
{
Data = data,
PageNumber = page,
PageSize = pageSize,
TotalCount = totalCount,
TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize)
TotalCount = pagedResult.TotalCount,
TotalPages = (int)Math.Ceiling(pagedResult.TotalCount / (double)pageSize)
};
return Ok(viewModel);

View file

@ -1,9 +1,13 @@
using Api.SeaHavenIndustries.DTOs;
using Data.SeaHavenIndustries;
using FluentValidation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SeaHaven.Services.Interfaces;
using SeaHaven.Services.DTOs;
using System.Security.Claims;
namespace Api.SeaHavenIndustries.Controllers
{
@ -13,12 +17,12 @@ namespace Api.SeaHavenIndustries.Controllers
public class AssetController : Controller
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly ApplicationDbContext _db;
public AssetController(UserManager<ApplicationUser> userManager, ApplicationDbContext db)
private readonly IAssetService _assetService;
public AssetController(UserManager<ApplicationUser> userManager, IAssetService assetService)
{
_userManager = userManager;
_db = db;
_assetService = assetService;
}
[HttpPost]
@ -27,22 +31,28 @@ namespace Api.SeaHavenIndustries.Controllers
{
try
{
Assets model = new Assets();
model.AssetCode = asset.AssetCode;
model.Name = asset.Name;
model.Description = asset.Description;
model.AccountId = asset.AccountId;
model.LocationId = asset.LocationId;
model.IsActive = asset.IsActive;
_db.Assets.Add(model);
await _db.SaveChangesAsync();
var createDto = new CreateAssetDTO
{
Name = asset.Name ?? throw new ArgumentException("Name is required"),
LocationId = asset.LocationId
};
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
await _assetService.CreateAssetAsync(createDto, userId);
return Ok(new DataResponse { Message = "Asset Created Successfully", Status = "200" });
}
catch (ValidationException vex)
{
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
return BadRequest(new Response { Status = "Validation Error", Message = errors });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
@ -52,28 +62,32 @@ namespace Api.SeaHavenIndustries.Controllers
{
try
{
var exist = _db.Assets.Where(w => w.Id == asset.Id).FirstOrDefault();
if (exist == null)
var updateDto = new UpdateAssetDTO
{
return BadRequest(new Response { Status = "Error", Message = "Asset not found!" });
}
Name = asset.Name,
LocationId = asset.LocationId
};
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
await _assetService.UpdateAssetAsync(asset.Id, updateDto, userId);
exist.AssetCode = asset.AssetCode;
exist.Name = asset.Name;
exist.Description = asset.Description;
exist.AccountId = asset.AccountId;
exist.LocationId = asset.LocationId;
exist.IsActive = asset.IsActive;
_db.Assets.Update(exist);
await _db.SaveChangesAsync();
return Ok(new DataResponse { Message = "Asset Updated Successfully", Status = "200" });
}
catch (ValidationException vex)
{
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
return BadRequest(new Response { Status = "Validation Error", Message = errors });
}
catch (KeyNotFoundException)
{
return NotFound(new Response { Status = "Error", Message = "Asset not found" });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
@ -83,23 +97,20 @@ namespace Api.SeaHavenIndustries.Controllers
{
try
{
var exist = _db.Assets.Where(w => w.Id == id).FirstOrDefault();
if (exist != null)
{
_db.Remove(exist);
}
else
{
return BadRequest(new Response { Status = "Error", Message = "Asset not found!" });
}
await _db.SaveChangesAsync();
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
await _assetService.DeleteAssetAsync(id, userId!);
return Ok(new DataResponse { Message = "Asset Deleted Successfully", Status = "200" });
}
catch (KeyNotFoundException)
{
return NotFound(new Response { Status = "Error", Message = "Asset not found" });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
@ -109,17 +120,13 @@ namespace Api.SeaHavenIndustries.Controllers
{
try
{
var asset = await _db.Assets
.Include(a => a.Account)
.Include(a => a.Location)
.Where(w => w.Id == id)
.FirstOrDefaultAsync();
var asset = await _assetService.GetAssetByIdWithDetailsAsync(id);
if (asset == null)
{
return NotFound(new Response { Status = "Error", Message = "Asset not found" });
}
return Ok(asset);
}
catch (Exception ex)
@ -130,48 +137,25 @@ namespace Api.SeaHavenIndustries.Controllers
[HttpGet]
[Route("GetAssetList")]
public IActionResult GetAssetList(string? search = "", int page = 1, int pageSize = 10)
public async Task<IActionResult> GetAssetList(string? search = "", int page = 1, int pageSize = 10)
{
// Build the query with search filter
var query = _db.Assets
.Include(a => a.Account)
.Include(a => a.Location)
.Where(s =>
string.IsNullOrEmpty(search) ||
(s.Name != null && s.Name.ToLower().Contains(search.ToLower())) ||
(s.AssetCode != null && s.AssetCode.ToLower().Contains(search.ToLower())) ||
(s.Description != null && s.Description.ToLower().Contains(search.ToLower()))
);
var pagedResult = await _assetService.GetAssetsPagedAsync(page, pageSize, search);
// Get total count AFTER applying search filter
var totalCount = query.Count();
// Get paginated data
var data = query
.Select(n => new
{
n.Id,
n.AssetCode,
n.Name,
n.Description,
n.IsActive,
AccountName = n.Account != null ? n.Account.Name : "",
LocationName = n.Location != null ? n.Location.Name : ""
})
.OrderByDescending(d => d.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToList();
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
var data = pagedResult.Items.Select(a => new
{
a.Id,
a.Name,
LocationName = a.LocationName ?? "",
a.CreatedDate
});
var viewModel = new Pagination_DTO
{
Data = data,
PageNumber = page,
PageSize = pageSize,
TotalCount = totalCount,
TotalPages = totalPages
TotalCount = pagedResult.TotalCount,
TotalPages = (int)Math.Ceiling(pagedResult.TotalCount / (double)pageSize)
};
return Ok(viewModel);

View file

@ -34,36 +34,45 @@ namespace Api.SeaHavenIndustries.Controllers
[Route("login")]
public async Task<IActionResult> Login([FromBody] LoginModel model)
{
var user = await _userManager.FindByNameAsync(model.Username ?? "");
if (user != null && user.IsDeleted != true && await _userManager.CheckPasswordAsync(user, model.Password ?? ""))
try
{
// Standard login without 2FA
var userRoles = await _userManager.GetRolesAsync(user);
var authClaims = new List<Claim>
var user = await _userManager.FindByNameAsync(model.Username ?? "");
if (user != null && user.IsDeleted != true && await _userManager.CheckPasswordAsync(user, model.Password ?? ""))
{
// Standard login without 2FA
var userRoles = await _userManager.GetRolesAsync(user);
var authClaims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.UserName ?? ""),
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
foreach (var userRole in userRoles)
{
authClaims.Add(new Claim(ClaimTypes.Role, userRole));
foreach (var userRole in userRoles)
{
authClaims.Add(new Claim(ClaimTypes.Role, userRole));
}
var token = GetToken(authClaims);
return Ok(new
{
token = new JwtSecurityTokenHandler().WriteToken(token),
expiration = token.ValidTo,
email = user.Email,
userRoles = userRoles.FirstOrDefault(),
phoneNumber = user.PhoneNumber,
fullname = user.FirstName + " " + user.LastName,
id = user.Id
});
}
var token = GetToken(authClaims);
return Ok(new
{
token = new JwtSecurityTokenHandler().WriteToken(token),
expiration = token.ValidTo,
email = user.Email,
userRoles = userRoles.FirstOrDefault(),
phoneNumber = user.PhoneNumber,
fullname = user.FirstName + " " + user.LastName,
id = user.Id
});
}
// Invalid credentials
return Unauthorized();
// Invalid credentials
return Unauthorized();
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
}
[Route("ChangePassword")]

View file

@ -1,22 +1,5 @@
/*
===================================================================================
CALENDAR/EVENTS MODULE - COMPLETE IMPLEMENTATION
===================================================================================
This file contains everything needed for the Calendar/Events module:
1. Events Model (Database Entity)
2. Event_DTO (for GET responses)
3. EditEvent_DTO (for POST/PUT requests)
4. CalendarController (API endpoints)
5. SQL Script for database table creation and sample data
Author: AI Agent
Date: 2026-03-30
===================================================================================
*/
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Api.SeaHavenIndustries.DTOs;
@ -26,116 +9,9 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
// ===================================================================================
// SECTION 1: DATABASE MODEL (Data.SeaHavenIndustries/Models/Events.cs)
// ===================================================================================
namespace Data.SeaHavenIndustries
{
public class Events : FullAuditEntity
{
public string Title { get; set; } = null!;
public string? Description { get; set; }
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
public string EventColor { get; set; } = "primary"; // primary, success, info, warning, danger
// Schedule
public DateTime StartDate { get; set; }
public string? StartTime { get; set; } // e.g., "08:00 AM"
public DateTime EndDate { get; set; }
public string? EndTime { get; set; } // e.g., "08:30 AM"
public bool AllDay { get; set; } = false;
}
}
// ===================================================================================
// SECTION 2: DTOs (Api.SeaHavenIndustries/DTOs/Event_DTO.cs)
// ===================================================================================
namespace Api.SeaHavenIndustries.DTOs
{
// DTO for GET responses
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; }
}
// DTO for POST/PUT requests (Create/Edit)
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;
}
}
// ===================================================================================
// SECTION 3: CONTROLLER (Api.SeaHavenIndustries/Controllers/CalendarController.cs)
// ===================================================================================
namespace Api.SeaHavenIndustries.Controllers
{
//[Authorize] // Uncomment when authentication is needed
[Authorize]
[ApiController]
[Route("api/Calendar")]
public class CalendarController : Controller
@ -280,7 +156,6 @@ namespace Api.SeaHavenIndustries.Controllers
}
}
// ===================================================================================
// DELETE EVENT
// ===================================================================================
@ -314,21 +189,22 @@ namespace Api.SeaHavenIndustries.Controllers
// ===================================================================================
// GET EVENTS LIST (WITH PAGINATION AND FILTERS)
// ===================================================================================
[HttpPost]
[HttpGet]
[Route("GetEventList")]
public IActionResult GetEventList(Pagination_DTO dto)
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(dto.Search))
if (!string.IsNullOrWhiteSpace(search))
{
var searchLower = search.ToLower();
query = query.Where(e =>
e.Title.Contains(dto.Search) ||
(e.Description != null && e.Description.Contains(dto.Search)) ||
(e.Location != null && e.Location.Contains(dto.Search))
e.Title.ToLower().Contains(searchLower) ||
(e.Description != null && e.Description.ToLower().Contains(searchLower)) ||
(e.Location != null && e.Location.ToLower().Contains(searchLower))
);
}
@ -339,8 +215,8 @@ namespace Api.SeaHavenIndustries.Controllers
var events = query
.OrderByDescending(e => e.StartDate)
.ThenByDescending(e => e.CreatedDate)
.Skip((dto.PageNumber - 1) * dto.PageSize)
.Take(dto.PageSize)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(e => new Event_DTO
{
Id = e.Id,
@ -428,126 +304,3 @@ namespace Api.SeaHavenIndustries.Controllers
}
}
/*
===================================================================================
SECTION 4: DATABASE SETUP
===================================================================================
STEP 1: Add Events DbSet to ApplicationDbContext.cs
-------------------------------------------------------
Add this line in the ApplicationDbContext class (around line 48):
public DbSet<Events> Events { get; set; }
STEP 2: Create Database Migration
-------------------------------------------------------
Run these commands in the terminal from the Api.SeaHavenIndustries project directory:
dotnet ef migrations add AddEventsTable --project ../Data.SeaHavenIndustries
dotnet ef database update
STEP 3: SQL Script for Manual Table Creation (Alternative to migrations)
-------------------------------------------------------
*/
-- SQL SCRIPT TO CREATE EVENTS TABLE
/*
CREATE TABLE Events (
Id INT PRIMARY KEY IDENTITY(1,1),
Title NVARCHAR(100) NOT NULL,
Description NVARCHAR(MAX) NULL,
Location NVARCHAR(150) NULL,
RecordID INT NULL,
RecordName NVARCHAR(255) NULL,
RecordTargetType NVARCHAR(50) NULL,
ReminderMinutes INT NOT NULL DEFAULT 15,
EventColor NVARCHAR(20) NOT NULL DEFAULT 'primary',
StartDate DATETIME NOT NULL,
StartTime NVARCHAR(10) NULL,
EndDate DATETIME NOT NULL,
EndTime NVARCHAR(10) NULL,
AllDay BIT NOT NULL DEFAULT 0,
-- Audit fields
IsDeleted BIT NULL,
createdby NVARCHAR(450) NULL,
DeleterUserId NVARCHAR(450) NULL,
DeletionTime DATETIME NULL,
CreatedDate DATETIME NULL,
LastModificationTime DATETIME NULL,
LastModifierUserId INT NULL
);
-- Insert sample data
INSERT INTO Events (Title, Description, Location, StartDate, StartTime, EndDate, EndTime, AllDay, EventColor, ReminderMinutes, CreatedDate, IsDeleted)
VALUES
('Team Meeting', 'Weekly team sync', 'Conference Room A', '2026-04-01', '10:00 AM', '2026-04-01', '11:00 AM', 0, 'primary', 15, GETDATE(), 0),
('Client Presentation', 'Q1 Results Review with ABC Corp', 'Virtual - Zoom', '2026-04-03', '02:00 PM', '2026-04-03', '03:30 PM', 0, 'success', 30, GETDATE(), 0),
('Project Deadline', 'Final delivery for Project Phoenix', NULL, '2026-04-05', NULL, '2026-04-05', NULL, 1, 'danger', 1440, GETDATE(), 0),
('Training Session', 'New system training for all employees', 'Main Office', '2026-04-08', '09:00 AM', '2026-04-08', '12:00 PM', 0, 'info', 60, GETDATE(), 0),
('Equipment Maintenance', 'Scheduled maintenance for all assets', 'Warehouse', '2026-04-10', '08:00 AM', '2026-04-10', '05:00 PM', 0, 'warning', 120, GETDATE(), 0);
*/
/*
===================================================================================
SECTION 5: IMPLEMENTATION CHECKLIST
===================================================================================
BACKEND SETUP:
--------------
1. ✓ Create Events.cs in Data.SeaHavenIndustries/Models/
2. ✓ Create Event_DTO.cs in Api.SeaHavenIndustries/DTOs/
3. ✓ Create EditEvent_DTO.cs in Api.SeaHavenIndustries/DTOs/
4. ✓ Create CalendarController.cs in Api.SeaHavenIndustries/Controllers/
5. ☐ Add DbSet<Events> to ApplicationDbContext.cs
6. ☐ Run migrations (or execute SQL script)
7. ☐ Test all endpoints using Postman/Swagger
FRONTEND SETUP (Next Step):
---------------------------
1. ☐ Create src/pages/calendar/CalendarPage.js (List view with FullCalendar)
2. ☐ Create src/pages/calendar/EventFormPage.js (Create/Edit form)
3. ☐ Create src/pages/calendar/api.js (API calls)
4. ☐ Add routes to App.js
5. ☐ Install FullCalendar: npm install @fullcalendar/react @fullcalendar/daygrid @fullcalendar/timegrid @fullcalendar/interaction
API ENDPOINTS SUMMARY:
---------------------
POST /api/Calendar/AddEvent - Create new event
POST /api/Calendar/EditEvent - Update existing event
GET /api/Calendar/GetEventById/{id} - Get single event by ID
DELETE /api/Calendar/DeleteEvent/{id} - Delete event (soft delete)
POST /api/Calendar/GetEventList - Get paginated list with search
GET /api/Calendar/GetEventsForCalendar - Get events for calendar view (with date range)
KEY FEATURES IMPLEMENTED:
-------------------------
✓ CRUD operations (Create, Read, Update, Delete)
✓ Pagination with search filter
✓ Date range filtering for calendar view
✓ Soft delete (IsDeleted flag)
✓ Related record linking (RecordID, RecordName, RecordTargetType)
✓ Color coding for calendar display
✓ All-day event support
✓ Reminder settings
✓ Proper error handling
✓ Consistent with existing controller patterns (AssetController)
✓ Fixed pagination bug (totalCount calculated AFTER filters)
NOTES:
------
- EventColor values: 'primary', 'success', 'info', 'warning', 'danger'
- ReminderMinutes: 0=Never, 5, 10, 15, 30, 60, 120, 240, 360, 720, 1440 (1 day)
- StartTime/EndTime format: "08:00 AM", "02:30 PM", etc.
- AllDay: When true, time fields can be null/ignored
- RecordTargetType: Can be "Account", "Contact", "WorkOrder", "FollowUp", etc.
===================================================================================
END OF FILE
===================================================================================
*/

View file

@ -1,10 +1,12 @@
using Api.SeaHavenIndustries.DTOs;
using Data.SeaHavenIndustries;
using FluentValidation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SendGrid.Helpers.Mail;
using SeaHaven.Services.Interfaces;
namespace Api.SeaHavenIndustries.Controllers
{
@ -15,11 +17,17 @@ namespace Api.SeaHavenIndustries.Controllers
public class ContactController : Controller
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly ApplicationDbContext _db;
public ContactController(UserManager<ApplicationUser> userManager, ApplicationDbContext db)
private readonly IContactService _contactService;
private readonly ILocationService _locationService;
public ContactController(
UserManager<ApplicationUser> userManager,
IContactService contactService,
ILocationService locationService)
{
_userManager = userManager;
_db = db;
_contactService = contactService;
_locationService = locationService;
}
[HttpPost]
[Route("AddContacts")]
@ -27,32 +35,22 @@ namespace Api.SeaHavenIndustries.Controllers
{
try
{
Contacts model = new Contacts();
model.Title = contacts.Title;
model.Owner = contacts.Owner;
model.FirstName = contacts.FirstName;
model.MiddleName = contacts.MiddleName;
model.LastName = contacts.LastName;
model.ContactType = contacts.ContactType;
model.PhoneNumber = contacts.PhoneNumber;
model.Email = contacts.Email;
model.Address1 = contacts.Address1;
model.Address2 = contacts.Address2;
model.City = contacts.City;
model.State = contacts.State;
model.Zip = contacts.Zip;
model.FacebookUrl = contacts.FacebookUrl;
model.LinkedInUrl = contacts.LinkedInUrl;
model.TwitterUrl = contacts.TwitterUrl;
model.LocationId = contacts.LocationId;
model.AccountId = contacts.AccountId;
_db.Contacts.Add(model);
await _db.SaveChangesAsync();
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
var userId = _userManager.GetUserId(User);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
await _contactService.CreateContactAsync(contacts.ToServiceCreateDTO(), userId);
return Ok(new DataResponse { Message = "Contact created successfully", Status = "200" });
}
catch (ValidationException vex)
{
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
return BadRequest(new Response { Status = "Validation Error", Message = errors });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
@ -62,86 +60,66 @@ namespace Api.SeaHavenIndustries.Controllers
{
try
{
var exist = _db.Contacts.Where(w => w.Id == contacts.Id).FirstOrDefault();
if (exist == null)
return BadRequest(new Response { Status = "Error", Message = "Contact not found" });
var userId = _userManager.GetUserId(User);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
exist.Title = contacts.Title;
exist.Owner = contacts.Owner;
exist.FirstName = contacts.FirstName;
exist.MiddleName = contacts.MiddleName;
exist.LastName = contacts.LastName;
exist.ContactType = contacts.ContactType;
exist.PhoneNumber = contacts.PhoneNumber;
exist.Email = contacts.Email;
exist.Address1 = contacts.Address1;
exist.Address2 = contacts.Address2;
exist.City = contacts.City;
exist.State = contacts.State;
exist.Zip = contacts.Zip;
exist.FacebookUrl = contacts.FacebookUrl;
exist.LinkedInUrl = contacts.LinkedInUrl;
exist.TwitterUrl = contacts.TwitterUrl;
exist.LocationId = contacts.LocationId;
exist.AccountId = contacts.AccountId;
_db.Contacts.Update(exist);
await _db.SaveChangesAsync();
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
await _contactService.UpdateContactAsync(contacts.Id, contacts.ToServiceUpdateDTO(), userId);
return Ok(new DataResponse { Message = "Contact updated successfully", Status = "200" });
}
catch (ValidationException vex)
{
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
return BadRequest(new Response { Status = "Validation Error", Message = errors });
}
catch (KeyNotFoundException)
{
return NotFound(new Response { Status = "Error", Message = "Contact not found" });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
[HttpGet]
[Route("DeleteContacts")]
public async Task<IActionResult> DeleteContacts(int id)
{
try
{
var exist = _db.Contacts.Where(w => w.Id == id).FirstOrDefault();
if (exist != null)
{
_db.Remove(exist);
}
else
{
return BadRequest(new Response { Status = "Error", Message = "ID not matched!" });
}
await _db.SaveChangesAsync();
return Ok(new DataResponse { Message = "Removed Successfully", Status = "200" });
var userId = _userManager.GetUserId(User);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
await _contactService.DeleteContactAsync(id, userId);
return Ok(new DataResponse { Message = "Contact deleted successfully", Status = "200" });
}
catch (KeyNotFoundException)
{
return NotFound(new Response { Status = "Error", Message = "Contact not found" });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
[HttpGet]
[Route("GetContactList")]
public IActionResult GetContactList(string? search ="", int page = 1, int pageSize = 10)
public async Task<IActionResult> GetContactList(string? search ="", int page = 1, int pageSize = 10)
{
var totalCount = _db.Contacts.Count();
var data = _db.Contacts.Where(s=>
(s.FirstName != null && s.FirstName.ToLower().Contains(search.ToLower())) ||
(s.LastName != null && s.LastName.ToLower().Contains(search.ToLower())) ||
(s.Email != null && s.Email.ToLower().Contains(search.ToLower())) ||
(s.PhoneNumber != null && s.PhoneNumber.Contains(search.ToLower()))
).Select(n => new
{
n.Id,
Name = (n.FirstName + " " + n.LastName).Trim(),
email = n.Email,
phone = n.PhoneNumber
}).OrderByDescending(d => d.Id).Skip((page - 1) * pageSize)
.Take(pageSize).ToList();
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
var pagedResult = await _contactService.GetContactsPagedAsync(page, pageSize, search);
var data = ContactListItem_DTO.FromServiceDTOs(pagedResult.Items);
var totalPages = (int)Math.Ceiling(pagedResult.TotalCount / (double)pageSize);
var viewModel = new Pagination_DTO
{
Data = data,
PageNumber = page,
PageSize = pageSize,
TotalCount = totalCount,
TotalCount = pagedResult.TotalCount,
TotalPages = totalPages
};
return Ok(viewModel);
@ -151,18 +129,15 @@ namespace Api.SeaHavenIndustries.Controllers
[Route("GetContacts")]
public async Task<IActionResult> GetContacts()
{
var data = await _db.Contacts.ToListAsync();
var data = await _contactService.GetAllContactsAsync();
return Ok(data);
}
[HttpGet]
[Route("GetLocations")]
public async Task<IActionResult> GetLocations()
{
var data = await _db.Locations.Select(s => new LocationViewModel
{
Id = s.Id,
Name = s.Title + " - " + s.Name
}).ToListAsync();
var locations = await _locationService.GetAllLocationsAsync();
var data = LocationViewModel_DTO.FromServiceDTOs(locations);
return Ok(data);
}
@ -170,33 +145,24 @@ namespace Api.SeaHavenIndustries.Controllers
[Route("GetCategories")]
public async Task<IActionResult> GetCategories()
{
var data = await _db.Categories.ToListAsync();
return Ok(data);
return StatusCode(501, new Response { Status = "Error", Message = "Not Implemented - Needs CategoryService" });
}
[HttpGet]
[Route("GetUsers")]
public async Task<IActionResult> GetUsers()
{
var filteredUsers = await (from user in _db.Users
where !(from userRole in _db.UserRoles
join role in _db.Roles on userRole.RoleId equals role.Id
where role.Name == "Admin" || role.Name == "Manager"
select userRole.UserId).Contains(user.Id)
select user).ToListAsync();
return Ok(filteredUsers);
return StatusCode(501, new Response { Status = "Error", Message = "Not Implemented - Needs UserService" });
}
[HttpPost]
[Route("AddLocation")]
public async Task<IActionResult> AddLocation(Location_DTO model)
{
try
{
Locations locations = new Locations();
locations.Title = model.Title;
locations.Name = model.Name;
_db.Locations.Add(locations);
await _db.SaveChangesAsync();
var userId = _userManager.GetUserId(User) ?? "system";
await _locationService.CreateLocationAsync(model.ToServiceCreateDTO(), userId);
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
}
catch (Exception ex)
@ -204,26 +170,21 @@ namespace Api.SeaHavenIndustries.Controllers
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
[HttpPost]
[Route("EditLocation")]
public async Task<IActionResult> EditLocation(EditLocation_DTO model)
{
try
{
var exist = _db.Locations.Where(w => w.Id == model.Id).FirstOrDefault();
if (exist != null)
{
exist.Title = model.Title;
exist.Name = model.Name;
}
else
{
return BadRequest(new Response { Status = "Error", Message = "ID not matched!" });
}
_db.Locations.Update(exist);
await _db.SaveChangesAsync();
var userId = _userManager.GetUserId(User) ?? "system";
await _locationService.UpdateLocationAsync(model.Id, model.ToServiceUpdateDTO(), userId);
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
}
catch (KeyNotFoundException)
{
return BadRequest(new Response { Status = "Error", Message = "ID not matched!" });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
@ -236,20 +197,14 @@ namespace Api.SeaHavenIndustries.Controllers
{
try
{
var data1 = await _db.Templates.ToListAsync();
_db.Templates.RemoveRange(data1);
var data = await _db.Locations.Where(w => w.Id == Id).FirstOrDefaultAsync();
if (data != null)
{
_db.Locations.Remove(data ?? new Locations());
}
else
{
return BadRequest(new Response { Status = "Error", Message = "ID not matched!" });
}
await _db.SaveChangesAsync();
var userId = _userManager.GetUserId(User) ?? "system";
await _locationService.DeleteLocationAsync(Id, userId);
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
}
catch (KeyNotFoundException)
{
return BadRequest(new Response { Status = "Error", Message = "ID not matched!" });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
@ -258,34 +213,22 @@ namespace Api.SeaHavenIndustries.Controllers
[HttpGet]
[Route("GetAddressbook")]
public IActionResult GetAddressbook(string? search = "", int page = 1, int pageSize = 10)
public async Task<IActionResult> GetAddressbook(string? search = "", int page = 1, int pageSize = 10)
{
var totalCount = _db.Locations.Count();
var data = _db.Locations.Where(w=> (w.Name ?? "").ToLower().Contains(search.ToLower()) || (w.Title ?? "").ToLower().Contains(search.ToLower())).Select(n => new
{
n.Id,
n.Name,
n.Title
}).OrderByDescending(d => d.Id).Skip((page - 1) * pageSize)
.Take(pageSize).ToList();
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
var pagedResult = await _locationService.GetLocationsPagedAsync(page, pageSize, search);
var data = LocationListItem_DTO.FromServiceDTOs(pagedResult.Items);
var totalPages = (int)Math.Ceiling(pagedResult.TotalCount / (double)pageSize);
var viewModel = new Pagination_DTO
{
Data = data,
PageNumber = page,
PageSize = pageSize,
TotalCount = totalCount,
TotalCount = pagedResult.TotalCount,
TotalPages = totalPages
};
return Ok(viewModel);
}
public class LocationViewModel
{
public int? Id { get; set; }
public string? Name { get; set; }
}
}
}

View file

@ -2,6 +2,8 @@ using Data.SeaHavenIndustries;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SeaHaven.Services.Interfaces;
using SeaHaven.Services.DTOs;
namespace Api.SeaHavenIndustries.Controllers
{
@ -11,10 +13,12 @@ namespace Api.SeaHavenIndustries.Controllers
[Route("api/dispatches")]
public class DispatchController : Controller
{
private readonly ApplicationDbContext _db;
private readonly IDispatchService _dispatchService;
private readonly ApplicationDbContext _db; // Keep for complex queries
public DispatchController(ApplicationDbContext db)
public DispatchController(IDispatchService dispatchService, ApplicationDbContext db)
{
_dispatchService = dispatchService;
_db = db;
}
@ -327,10 +331,10 @@ namespace Api.SeaHavenIndustries.Controllers
{
dispatch.WorkOrder.Locations.Id,
dispatch.WorkOrder.Locations.Name,
dispatch.WorkOrder.Locations.Address,
dispatch.WorkOrder.Locations.Address1,
dispatch.WorkOrder.Locations.City,
dispatch.WorkOrder.Locations.State,
dispatch.WorkOrder.Locations.ZipCode
dispatch.WorkOrder.Locations.Zip
},
WorkOrders = linkedWOs,
Checklist = checklist,

View file

@ -1,176 +1,180 @@
using Api.SeaHavenIndustries.DTOs;
using Data.SeaHavenIndustries;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SeaHaven.Services.Interfaces;
using SeaHaven.Services.DTOs;
using System.Security.Claims;
[ApiController]
[Route("api/Employee")]
[Route("api/employees")]
public class EmployeeController : Controller
{
private readonly ApplicationDbContext _db;
private readonly IEmployeeService _employeeService;
public EmployeeController(ApplicationDbContext db)
public EmployeeController(IEmployeeService employeeService)
{
_db = db;
_employeeService = employeeService;
}
// -----------------------------------
// POST: api/Employee/AddEmployees
// POST: api/Employee/AddEmployees or /Create
// -----------------------------------
[HttpPost("AddEmployees")]
[HttpPost("Create")]
public async Task<IActionResult> AddEmployees(AddEmployee_DTO dto)
{
var employee = new Employee
try
{
FirstName = dto.FirstName,
LastName = dto.LastName,
JobTitleId = dto.JobTitleId,
PermissionGroupId = dto.PermissionGroupId,
Status = dto.Status,
LastLogin = dto.LastLogin,
CreatedDate = DateTime.UtcNow
};
if (!string.IsNullOrWhiteSpace(dto.Email))
{
employee.Emails.Add(new EmployeeEmail
var createDto = new CreateEmployeeDTO
{
Address = dto.Email,
IsPrimary = true,
EmailTypeId = 1
});
}
FirstName = dto.FirstName,
LastName = dto.LastName,
JobTitleId = dto.JobTitleId,
DepartmentId = null, // Map from PermissionGroupId if needed
Email = dto.Email,
Phone = dto.Phone
};
if (!string.IsNullOrWhiteSpace(dto.Phone))
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
await _employeeService.CreateEmployeeAsync(createDto, userId);
return Ok(new { Message = "Employee added successfully" });
}
catch (ValidationException vex)
{
employee.Phones.Add(new EmployeePhone
{
Number = dto.Phone,
Extension = dto.PhoneExtension,
IsPrimary = true,
PhoneTypeId = 1
});
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
return BadRequest(new Response { Status = "Validation Error", Message = errors });
}
catch (Exception ex)
{
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
_db.Employees.Add(employee);
await _db.SaveChangesAsync();
return Ok(new { Message = "Employee added successfully" });
}
// -----------------------------------
// POST: api/Employee/EditEmployees
// POST: api/Employee/EditEmployees or /Update
// -----------------------------------
[HttpPost("EditEmployees")]
[HttpPost("Update")]
public async Task<IActionResult> EditEmployees(EditEmployee_DTO dto)
{
var employee = await _db.Employees
.Include(e => e.Emails)
.Include(e => e.Phones)
.FirstOrDefaultAsync(e => e.Id == dto.Id);
try
{
var updateDto = new UpdateEmployeeDTO
{
FirstName = dto.FirstName,
LastName = dto.LastName,
JobTitleId = dto.JobTitleId
};
if (employee == null)
return BadRequest("Employee not found");
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
employee.FirstName = dto.FirstName;
employee.LastName = dto.LastName;
employee.JobTitleId = dto.JobTitleId;
employee.PermissionGroupId = dto.PermissionGroupId;
employee.Status = dto.Status;
employee.LastLogin = dto.LastLogin;
employee.LastModificationTime = DateTime.UtcNow;
await _employeeService.UpdateEmployeeAsync(dto.Id, updateDto, userId);
await _db.SaveChangesAsync();
return Ok(new { Message = "Employee updated successfully" });
return Ok(new { Message = "Employee updated successfully" });
}
catch (ValidationException vex)
{
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
return BadRequest(new Response { Status = "Validation Error", Message = errors });
}
catch (KeyNotFoundException)
{
return NotFound(new Response { Status = "Error", Message = "Employee not found" });
}
catch (Exception ex)
{
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
// -----------------------------------
// GET: api/Employee/GetEmployeeList
// -----------------------------------
[HttpGet("GetEmployeeList")]
public IActionResult GetEmployeeList(string? search = "", int page = 1, int pageSize = 12)
public async Task<IActionResult> GetEmployeeList(string? search = "", int page = 1, int pageSize = 12)
{
search ??= "";
var pagedResult = await _employeeService.GetEmployeesPagedAsync(page, pageSize, search);
var totalCount = _db.Employees.Where(e => !e.IsDeleted.HasValue || e.IsDeleted == false).Count();
var data = pagedResult.Items.Select(e => new
{
id = e.Id,
firstName = e.FirstName,
lastName = e.LastName,
jobTitle = e.JobTitleName ?? "",
email = e.Email ?? "",
phone = e.Phone ?? "",
department = e.DepartmentName ?? "",
status = "Active" // Map from employee status if available
});
var data = _db.Employees
.Include(e => e.JobTitle)
.Include(e => e.Department)
.Include(e => e.Emails)
.Include(e => e.Phones)
.Where(e => !e.IsDeleted.HasValue || e.IsDeleted == false)
.Where(e =>
string.IsNullOrWhiteSpace(search) ||
(e.FirstName != null && e.FirstName.ToLower().Contains(search.ToLower())) ||
(e.LastName != null && e.LastName.ToLower().Contains(search.ToLower())) ||
e.Emails.Any(x => x.Address != null && x.Address.ToLower().Contains(search.ToLower())) ||
e.Phones.Any(x => x.Number != null && x.Number.Contains(search))
)
.Select(e => new
{
e.Id,
e.FirstName,
e.LastName,
JobTitleName = e.JobTitle != null ? e.JobTitle.Name : "",
DepartmentName = e.Department != null ? e.Department.Name : "",
e.Status,
PrimaryEmail = e.Emails.Where(x => x.IsPrimary == true).Select(x => x.Address).FirstOrDefault(),
PrimaryPhone = e.Phones.Where(x => x.IsPrimary == true).Select(x => x.Number).FirstOrDefault(),
PhoneExtension = e.Phones.Where(x => x.IsPrimary == true).Select(x => x.Extension).FirstOrDefault()
})
.OrderByDescending(e => e.Id)
.ToList()
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(e => new
{
id = e.Id,
firstName = e.FirstName,
lastName = e.LastName,
jobTitle = e.JobTitleName ?? "",
email = e.PrimaryEmail ?? "",
phone = e.PrimaryPhone != null
? e.PrimaryPhone + (e.PhoneExtension != null ? " ext. " + e.PhoneExtension : "")
: "",
department = e.DepartmentName ?? "",
status = e.Status
});
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
var totalPages = (int)Math.Ceiling(pagedResult.TotalCount / (double)pageSize);
var viewModel = new Pagination_DTO
{
Data = data,
PageNumber = page,
PageSize = pageSize,
TotalCount = totalCount,
TotalCount = pagedResult.TotalCount,
TotalPages = totalPages
};
return Ok(viewModel);
}
// -----------------------------------
// GET: api/Employee/DeleteEmployees?id=5
// GET: api/Employee/GetById?id=5
// -----------------------------------
[HttpGet("GetById")]
public async Task<IActionResult> GetById(int id)
{
try
{
var employee = await _employeeService.GetEmployeeByIdAsync(id);
return Ok(new
{
id = employee.Id,
firstName = employee.FirstName,
lastName = employee.LastName,
jobTitle = employee.JobTitleName ?? "",
email = employee.Email ?? "",
phone = employee.Phone ?? "",
department = employee.DepartmentName ?? "",
status = "Active"
});
}
catch (KeyNotFoundException)
{
return NotFound(new Response { Status = "Error", Message = "Employee not found" });
}
catch (Exception ex)
{
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
// -----------------------------------
// GET: api/Employee/DeleteEmployees or /Delete?id=5
// -----------------------------------
[HttpGet("DeleteEmployees")]
[HttpPost("Delete")]
public async Task<IActionResult> DeleteEmployees(int id)
{
try
{
var employee = await _db.Employees
.FirstOrDefaultAsync(e => e.Id == id);
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
if (employee == null)
return BadRequest(new Response
{
Status = "Error",
Message = "Employee not found."
});
_db.Employees.Remove(employee);
await _db.SaveChangesAsync();
await _employeeService.DeleteEmployeeAsync(id, userId);
return Ok(new DataResponse
{
@ -178,7 +182,7 @@ public class EmployeeController : Controller
Status = "200"
});
}
catch (Exception ex)
catch (InvalidOperationException ex)
{
return BadRequest(new Response
{
@ -186,6 +190,31 @@ public class EmployeeController : Controller
Message = ex.Message
});
}
catch (KeyNotFoundException)
{
return NotFound(new Response { Status = "Error", Message = "Employee not found" });
}
catch (Exception ex)
{
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
// -----------------------------------
// GET: api/Employee/Dropdown
// -----------------------------------
[HttpGet("Dropdown")]
public async Task<IActionResult> GetDropdown()
{
var pagedResult = await _employeeService.GetEmployeesPagedAsync(1, 1000, "");
var data = pagedResult.Items.Select(e => new
{
id = e.Id,
name = $"{e.FirstName} {e.LastName}".Trim()
});
return Ok(data);
}
}

View file

@ -1,10 +1,14 @@
using System.Security.Claims;
using Api.SeaHavenIndustries.DTOs;
using Data.SeaHavenIndustries;
using FluentValidation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SeaHaven.Services.Interfaces;
using SeaHaven.Services.DTOs;
using SeaHaven.DataServices.Interfaces;
[ApiController]
[Authorize]
@ -12,11 +16,15 @@ using Microsoft.EntityFrameworkCore;
[Route("api/followups")]
public class FollowUpController : ControllerBase
{
private readonly ApplicationDbContext _db;
private readonly IFollowUpService _followUpService;
private readonly IFollowUpDataService _followUpDataService; // Keep for direct entity operations
private readonly ApplicationDbContext _db; // Keep for User lookups
private readonly UserManager<ApplicationUser> _users;
public FollowUpController(ApplicationDbContext db, UserManager<ApplicationUser> users)
public FollowUpController(IFollowUpService followUpService, IFollowUpDataService followUpDataService, ApplicationDbContext db, UserManager<ApplicationUser> users)
{
_followUpService = followUpService;
_followUpDataService = followUpDataService;
_db = db;
_users = users;
}
@ -30,55 +38,28 @@ public class FollowUpController : ControllerBase
{
status = string.IsNullOrWhiteSpace(status) ? "Open" : status;
var query = _db.FollowUps
.Include(x => x.Employee)
.Include(x => x.Account)
.Include(x => x.Location)
.Include(x => x.WorkOrder)
.Where(x => x.Status == status);
var pagedResult = await _followUpService.GetFollowUpsPagedAsync(page, pageSize, search, status);
var items = pagedResult.Items;
if (!string.IsNullOrWhiteSpace(search))
var rows = items.Select(x => new
{
var s = search.ToLower();
query = query.Where(x =>
(x.Reason ?? "").ToLower().Contains(s) ||
(x.Account != null && (x.Account.Name ?? "").ToLower().Contains(s)) ||
(x.Employee != null &&
(((x.Employee.FirstName ?? "") + " " + (x.Employee.LastName ?? ""))
.ToLower().Contains(s))
)
);
}
var totalCount = await query.CountAsync();
var rows = await query
.OrderByDescending(x => x.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(x => new
{
x.Id,
x.Reason,
x.ScheduleDate,
x.ScheduleStartTime,
x.EmployeeId,
x.WorkOrderId,
x.AccountId,
x.LocationId,
AccountName = x.Account != null ? x.Account.Name : "",
AssignedTo = x.Employee != null
? (x.Employee.FirstName + " " + x.Employee.LastName)
: "",
LocationName = x.Location != null ? x.Location.Name : "",
WorkOrderNumber = x.WorkOrder != null ? x.WorkOrder.WorkerOrderNumber : "",
InternalWONumber = x.WorkOrder != null ? x.WorkOrder.InternalWONumber : "",
x.Status,
x.CreatedDate,
CreatedById = x.createdby
})
.ToListAsync();
Id = x.Id,
Reason = x.Reason,
ScheduleDate = x.ScheduleDate,
ScheduleStartTime = x.ScheduleStartTime,
EmployeeId = x.EmployeeId,
WorkOrderId = x.WorkOrderId,
AccountId = x.AccountId,
LocationId = x.LocationId,
AccountName = x.AccountName ?? "",
AssignedTo = x.EmployeeName ?? "",
LocationName = x.LocationName ?? "",
WorkOrderNumber = "",
InternalWONumber = "",
Status = x.Status,
CreatedDate = x.CreatedDate,
CreatedById = (string?)null
}).ToList();
var creatorIds = rows
.Select(r => r.CreatedById)
@ -124,20 +105,15 @@ public class FollowUpController : ControllerBase
Data = data,
PageNumber = page,
PageSize = pageSize,
TotalCount = totalCount,
TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize)
TotalCount = pagedResult.TotalCount,
TotalPages = (int)Math.Ceiling(pagedResult.TotalCount / (double)pageSize)
});
}
[HttpGet("GetById")]
public async Task<IActionResult> GetById(int id)
{
var model = await _db.FollowUps
.Include(x => x.Employee)
.Include(x => x.Account)
.Include(x => x.Location)
.Include(x => x.WorkOrder)
.FirstOrDefaultAsync(x => x.Id == id);
var model = await _followUpDataService.GetByIdWithDetailsAsync(id);
if (model == null) return NotFound();
@ -182,56 +158,86 @@ public class FollowUpController : ControllerBase
[HttpPost("Create")]
public async Task<IActionResult> Create(FollowUp_DTO dto)
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var model = new FollowUps
try
{
Reason = dto.Reason,
ScheduleDate = dto.ScheduleDate,
ScheduleStartTime = dto.ScheduleStartTime,
EmployeeId = dto.EmployeeId,
WorkOrderId = dto.WorkOrderId,
AccountId = dto.AccountId,
LocationId = dto.LocationId,
Status = "Open",
CreatedDate = DateTime.UtcNow,
createdby = userId
};
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
_db.FollowUps.Add(model);
await _db.SaveChangesAsync();
var model = new FollowUps
{
Reason = dto.Reason,
ScheduleDate = dto.ScheduleDate,
ScheduleStartTime = dto.ScheduleStartTime,
EmployeeId = dto.EmployeeId,
WorkOrderId = dto.WorkOrderId,
AccountId = dto.AccountId,
LocationId = dto.LocationId,
Status = "Open",
CreatedDate = DateTime.UtcNow,
createdby = userId
};
return Ok(new DataResponse { Status = "200", Message = "Follow Up created", Data = model.Id });
await _followUpDataService.AddAsync(model);
return Ok(new DataResponse { Status = "200", Message = "Follow Up created", Data = model.Id });
}
catch (ValidationException vex)
{
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
return BadRequest(new Response { Status = "Validation Error", Message = errors });
}
catch (Exception ex)
{
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
[HttpPost("Update")]
public async Task<IActionResult> Update(FollowUp_DTO dto)
{
var model = await _db.FollowUps.FindAsync(dto.Id);
if (model == null) return NotFound();
try
{
var model = await _followUpDataService.GetByIdAsync(dto.Id);
if (model == null)
return NotFound(new Response { Status = "Error", Message = "Follow Up not found" });
model.Reason = dto.Reason;
model.ScheduleDate = dto.ScheduleDate;
model.ScheduleStartTime = dto.ScheduleStartTime;
model.EmployeeId = dto.EmployeeId;
model.WorkOrderId = dto.WorkOrderId;
model.AccountId = dto.AccountId;
model.LocationId = dto.LocationId;
model.LastModificationTime = DateTime.UtcNow;
model.Reason = dto.Reason;
model.ScheduleDate = dto.ScheduleDate;
model.ScheduleStartTime = dto.ScheduleStartTime;
model.EmployeeId = dto.EmployeeId;
model.WorkOrderId = dto.WorkOrderId;
model.AccountId = dto.AccountId;
model.LocationId = dto.LocationId;
model.LastModificationTime = DateTime.UtcNow;
await _db.SaveChangesAsync();
return Ok(new DataResponse { Status = "200", Message = "Follow Up updated" });
await _followUpDataService.UpdateAsync(model);
return Ok(new DataResponse { Status = "200", Message = "Follow Up updated" });
}
catch (ValidationException vex)
{
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
return BadRequest(new Response { Status = "Validation Error", Message = errors });
}
catch (KeyNotFoundException)
{
return NotFound(new Response { Status = "Error", Message = "Follow Up not found" });
}
catch (Exception ex)
{
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
[HttpPost("Complete")]
public async Task<IActionResult> Complete(int id)
{
var model = await _db.FollowUps.FindAsync(id);
var model = await _followUpDataService.GetByIdAsync(id);
if (model == null) return NotFound();
model.Status = "Completed";
model.LastModificationTime = DateTime.UtcNow;
await _db.SaveChangesAsync();
await _followUpDataService.UpdateAsync(model);
return Ok();
}
@ -239,12 +245,12 @@ public class FollowUpController : ControllerBase
[HttpPost("Cancel")]
public async Task<IActionResult> Cancel(int id)
{
var model = await _db.FollowUps.FindAsync(id);
var model = await _followUpDataService.GetByIdAsync(id);
if (model == null) return NotFound();
model.Status = "Cancelled";
model.LastModificationTime = DateTime.UtcNow;
await _db.SaveChangesAsync();
await _followUpDataService.UpdateAsync(model);
return Ok();
}
@ -252,12 +258,23 @@ public class FollowUpController : ControllerBase
[HttpPost("Delete")]
public async Task<IActionResult> Delete(int id)
{
var model = await _db.FollowUps.FindAsync(id);
if (model == null) return NotFound();
try
{
var model = await _followUpDataService.GetByIdAsync(id);
if (model == null)
return NotFound(new Response { Status = "Error", Message = "Follow Up not found" });
_db.FollowUps.Remove(model);
await _db.SaveChangesAsync();
await _followUpDataService.DeleteAsync(id);
return Ok(new DataResponse { Status = "200", Message = "Follow Up deleted" });
return Ok(new DataResponse { Status = "200", Message = "Follow Up deleted" });
}
catch (KeyNotFoundException)
{
return NotFound(new Response { Status = "Error", Message = "Follow Up not found" });
}
catch (Exception ex)
{
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
}

View file

@ -1,8 +1,13 @@
using Api.SeaHavenIndustries.DTOs;
using Data.SeaHavenIndustries;
using FluentValidation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SeaHaven.Services.Interfaces;
using SeaHaven.Services.DTOs;
using SeaHaven.DataServices.Interfaces;
using System.Security.Claims;
namespace Api.SeaHavenIndustries.Controllers
{
@ -12,58 +17,44 @@ namespace Api.SeaHavenIndustries.Controllers
[Route("api/locations")]
public class LocationController : Controller
{
private readonly ApplicationDbContext _db;
private readonly ILocationService _locationService;
private readonly ILocationDataService _locationDataService; // Keep for direct entity operations
public LocationController(ApplicationDbContext db)
public LocationController(ILocationService locationService, ILocationDataService locationDataService)
{
_db = db;
_locationService = locationService;
_locationDataService = locationDataService;
}
[HttpGet("GetLocationList")]
public IActionResult GetLocationList(string? search = "", int page = 1, int pageSize = 10)
public async Task<IActionResult> GetLocationList(string? search = "", int page = 1, int pageSize = 10)
{
var query = _db.Locations.AsQueryable();
var pagedResult = await _locationService.GetLocationsPagedAsync(page, pageSize, search);
if (!string.IsNullOrWhiteSpace(search))
var data = pagedResult.Items.Select(l => new
{
var s = search.ToLower();
query = query.Where(l =>
(l.Name ?? "").ToLower().Contains(s) ||
(l.Title ?? "").ToLower().Contains(s) ||
(l.Address ?? "").ToLower().Contains(s) ||
(l.City ?? "").ToLower().Contains(s));
}
var totalCount = query.Count();
var data = query
.OrderByDescending(l => l.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(l => new
{
l.Id,
l.Name,
l.Title,
l.Address,
l.City,
l.State,
l.ZipCode,
l.Phone,
l.Contact,
l.ContactEmail,
l.Status,
l.AccountId
})
.ToList();
Id = l.Id,
Name = l.Name,
Title = l.Title,
Address = l.Address1,
Address1 = l.Address1,
Address2 = l.Address2,
City = l.City,
State = l.State,
ZipCode = l.Zip,
Phone = l.PhoneNumber,
Contact = (string?)null,
ContactEmail = l.Email,
Status = l.Status
});
var viewModel = new Pagination_DTO
{
Data = data,
PageNumber = page,
PageSize = pageSize,
TotalCount = totalCount,
TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize)
TotalCount = pagedResult.TotalCount,
TotalPages = (int)Math.Ceiling(pagedResult.TotalCount / (double)pageSize)
};
return Ok(viewModel);
@ -72,29 +63,29 @@ namespace Api.SeaHavenIndustries.Controllers
[HttpGet("{id}")]
public async Task<IActionResult> GetLocationById(int id)
{
var location = await _db.Locations
.Where(l => l.Id == id)
.Select(l => new
{
l.Id,
l.Name,
l.Title,
l.Address,
l.City,
l.State,
l.ZipCode,
l.Phone,
l.Contact,
l.ContactEmail,
l.Status,
l.AccountId
})
.FirstOrDefaultAsync();
var location = await _locationDataService.GetByIdAsync(id);
if (location == null)
return NotFound(new Response { Status = "Error", Message = "Location not found" });
return Ok(location);
var result = new
{
location.Id,
location.Name,
location.Title,
Address = location.Address1,
location.Address1,
location.Address2,
location.City,
location.State,
ZipCode = location.Zip,
Phone = location.PhoneNumber,
Contact = (string?)null,
ContactEmail = location.Email,
location.Status
};
return Ok(result);
}
[HttpPost]
@ -106,24 +97,27 @@ namespace Api.SeaHavenIndustries.Controllers
{
Name = model.Name,
Title = model.Title,
Address = model.Address,
Address1 = model.Address,
City = model.City,
State = model.State,
ZipCode = model.ZipCode,
Phone = model.Phone,
Contact = model.Contact,
ContactEmail = model.ContactEmail,
Zip = model.ZipCode,
PhoneNumber = model.Phone,
Email = model.ContactEmail,
Status = model.Status,
AccountId = model.GetAccountId()
CreatedDate = DateTime.Now
};
_db.Locations.Add(location);
await _db.SaveChangesAsync();
await _locationDataService.AddAsync(location);
return Ok(new DataResponse { Message = "Location Created Successfully", Status = "200" });
}
catch (ValidationException vex)
{
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
return BadRequest(new Response { Status = "Validation Error", Message = errors });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
@ -132,28 +126,36 @@ namespace Api.SeaHavenIndustries.Controllers
{
try
{
var exist = await _db.Locations.Where(l => l.Id == id).FirstOrDefaultAsync();
var exist = await _locationDataService.GetByIdAsync(id);
if (exist == null)
return BadRequest(new Response { Status = "Error", Message = "Location not found" });
return NotFound(new Response { Status = "Error", Message = "Location not found" });
exist.Name = model.Name;
exist.Title = model.Title;
exist.Address = model.Address;
exist.Address1 = model.Address;
exist.City = model.City;
exist.State = model.State;
exist.ZipCode = model.ZipCode;
exist.Phone = model.Phone;
exist.Contact = model.Contact;
exist.ContactEmail = model.ContactEmail;
exist.Zip = model.ZipCode;
exist.PhoneNumber = model.Phone;
exist.Email = model.ContactEmail;
exist.Status = model.Status;
exist.AccountId = model.GetAccountId();
exist.LastModificationTime = DateTime.Now;
await _db.SaveChangesAsync();
await _locationDataService.UpdateAsync(exist);
return Ok(new DataResponse { Message = "Location Updated Successfully", Status = "200" });
}
catch (ValidationException vex)
{
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
return BadRequest(new Response { Status = "Validation Error", Message = errors });
}
catch (KeyNotFoundException)
{
return NotFound(new Response { Status = "Error", Message = "Location not found" });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
@ -162,17 +164,20 @@ namespace Api.SeaHavenIndustries.Controllers
{
try
{
var exist = await _db.Locations.Where(l => l.Id == id).FirstOrDefaultAsync();
var exist = await _locationDataService.GetByIdAsync(id);
if (exist == null)
return BadRequest(new Response { Status = "Error", Message = "Location not found" });
return NotFound(new Response { Status = "Error", Message = "Location not found" });
_db.Locations.Remove(exist);
await _db.SaveChangesAsync();
await _locationDataService.DeleteAsync(id);
return Ok(new DataResponse { Message = "Location Deleted Successfully", Status = "200" });
}
catch (KeyNotFoundException)
{
return NotFound(new Response { Status = "Error", Message = "Location not found" });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}

View file

@ -1,70 +1,147 @@
using Api.SeaHavenIndustries.DTOs;
using Data.SeaHavenIndustries;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SeaHaven.Services.Interfaces;
using SeaHaven.Services.DTOs;
using System.Security.Claims;
[ApiController]
[Route("api/PMSchedule")]
[Route("api/pmschedules")]
public class PMScheduleController : ControllerBase
{
private readonly ApplicationDbContext _db;
private readonly IPMScheduleService _pmScheduleService;
public PMScheduleController(ApplicationDbContext db)
public PMScheduleController(IPMScheduleService pmScheduleService)
{
_db = db;
_pmScheduleService = pmScheduleService;
}
[HttpGet("GetList")]
public IActionResult GetList()
public async Task<IActionResult> GetList()
{
var data = _db.PMSchedules
.Include(x => x.Asset)
.Include(x => x.Location)
.OrderByDescending(x => x.Id)
.Select(x => new
{
x.Id,
x.Name,
x.Frequency,
x.StartDate,
x.IsActive,
Asset = x.Asset != null ? x.Asset.Name : null,
Location = x.Location != null ? x.Location.Name : null
})
.ToList();
var items = await _pmScheduleService.GetAllPMSchedulesAsync();
var data = items.Select(x => new
{
Id = x.Id,
Name = x.Name,
Frequency = x.Frequency,
StartDate = x.StartDate,
IsActive = x.IsActive,
Asset = x.AssetName,
Location = x.LocationName
});
return Ok(data);
}
[HttpGet("GetById")]
public async Task<IActionResult> GetById([FromQuery] int id)
{
try
{
var pmSchedule = await _pmScheduleService.GetPMScheduleByIdAsync(id);
if (pmSchedule == null)
return NotFound(new Response { Status = "Error", Message = "PM Schedule not found" });
return Ok(new
{
Id = pmSchedule.Id,
Name = pmSchedule.Name,
Frequency = pmSchedule.Frequency,
StartDate = pmSchedule.StartDate,
IsActive = pmSchedule.IsActive,
AssetId = pmSchedule.AssetId,
LocationId = pmSchedule.LocationId,
Asset = pmSchedule.AssetName,
Location = pmSchedule.LocationName
});
}
catch (KeyNotFoundException)
{
return NotFound(new Response { Status = "Error", Message = "PM Schedule not found" });
}
catch (Exception ex)
{
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
[HttpPost("Save")]
public async Task<IActionResult> Save(PMSchedule_DTO dto)
{
PMSchedules model;
if (dto.Id == 0)
try
{
model = new PMSchedules
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
if (dto.Id == 0)
{
CreatedDate = DateTime.UtcNow
};
_db.PMSchedules.Add(model);
var createDto = new CreatePMScheduleDTO
{
Name = dto.Name,
Frequency = dto.Frequency,
StartDate = dto.StartDate,
IsActive = dto.IsActive,
AssetId = dto.AssetId,
LocationId = dto.LocationId
};
await _pmScheduleService.CreatePMScheduleAsync(createDto, userId);
}
else
{
var updateDto = new UpdatePMScheduleDTO
{
Name = dto.Name,
Frequency = dto.Frequency,
StartDate = dto.StartDate,
IsActive = dto.IsActive,
AssetId = dto.AssetId,
LocationId = dto.LocationId
};
await _pmScheduleService.UpdatePMScheduleAsync(dto.Id, updateDto, userId);
}
return Ok();
}
else
catch (ValidationException vex)
{
model = await _db.PMSchedules.FindAsync(dto.Id);
if (model == null) return NotFound();
model.LastModificationTime = DateTime.UtcNow;
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
return BadRequest(new Response { Status = "Validation Error", Message = errors });
}
catch (KeyNotFoundException)
{
return NotFound(new Response { Status = "Error", Message = "PM Schedule not found" });
}
catch (Exception ex)
{
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
model.Name = dto.Name;
model.Frequency = dto.Frequency;
model.StartDate = dto.StartDate;
model.IsActive = dto.IsActive;
model.AssetId = dto.AssetId;
model.LocationId = dto.LocationId;
[HttpPost("Delete")]
public async Task<IActionResult> Delete([FromQuery] int id)
{
try
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
await _db.SaveChangesAsync();
return Ok();
await _pmScheduleService.DeletePMScheduleAsync(id, userId);
return Ok(new DataResponse { Message = "PM Schedule deleted successfully", Status = "200" });
}
catch (KeyNotFoundException)
{
return NotFound(new Response { Status = "Error", Message = "PM Schedule not found" });
}
catch (Exception ex)
{
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
}

View file

@ -375,7 +375,7 @@ namespace Api.SeaHavenIndustries.Controllers
{
Name = matchCode,
Title = building,
Address = address,
Address1 = address,
Status = "Active"
};

View file

@ -1,8 +1,12 @@
using Api.SeaHavenIndustries.DTOs;
using Data.SeaHavenIndustries;
using FluentValidation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SeaHaven.Services.Interfaces;
using SeaHaven.Services.DTOs;
using System.Security.Claims;
namespace Api.SeaHavenIndustries.Controllers
{
@ -12,13 +16,15 @@ namespace Api.SeaHavenIndustries.Controllers
[Route("api/vendors")]
public class VendorController : Controller
{
private readonly ApplicationDbContext _db;
private readonly IVendorService _vendorService;
private readonly ApplicationDbContext _db; // Keep for complex queries like nearby vendors
private readonly Helper.ZipCodeDistance _zipDistance;
private readonly Helper.VendorPortalTokenService _vendorTokens;
private readonly IConfiguration _config;
public VendorController(ApplicationDbContext db, Helper.ZipCodeDistance zipDistance, Helper.VendorPortalTokenService vendorTokens, IConfiguration config)
public VendorController(IVendorService vendorService, ApplicationDbContext db, Helper.ZipCodeDistance zipDistance, Helper.VendorPortalTokenService vendorTokens, IConfiguration config)
{
_vendorService = vendorService;
_db = db;
_zipDistance = zipDistance;
_vendorTokens = vendorTokens;
@ -26,64 +32,36 @@ namespace Api.SeaHavenIndustries.Controllers
}
[HttpGet("GetVendorList")]
public IActionResult GetVendorList(string? search = "", int page = 1, int pageSize = 10)
public async Task<IActionResult> GetVendorList(string? search = "", int page = 1, int pageSize = 10)
{
var query = _db.Vendors.Where(v => v.IsActive);
var pagedResult = await _vendorService.GetVendorsPagedAsync(page, pageSize, search);
if (!string.IsNullOrWhiteSpace(search))
var data = pagedResult.Items.Select(v => new
{
var s = search.ToLower();
query = query.Where(v =>
(v.CompanyName ?? "").ToLower().Contains(s) ||
(v.ContactName ?? "").ToLower().Contains(s) ||
(v.Email ?? "").ToLower().Contains(s) ||
(v.TradeSpecialties ?? "").ToLower().Contains(s));
}
var totalCount = query.Count();
var data = query.OrderByDescending(v => v.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(v => new
{
v.Id,
v.CompanyName,
v.ContactName,
v.Email,
v.Phone,
v.TradeSpecialties,
v.IsActive
})
.ToList();
v.Id,
CompanyName = v.Name,
v.Email,
v.Phone
});
return Ok(new Pagination_DTO
{
Data = data,
PageNumber = page,
PageSize = pageSize,
TotalCount = totalCount,
TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize)
TotalCount = pagedResult.TotalCount,
TotalPages = (int)Math.Ceiling(pagedResult.TotalCount / (double)pageSize)
});
}
[HttpGet("{id}")]
public async Task<IActionResult> GetVendorById(int id)
[HttpGet("GetById")]
public async Task<IActionResult> GetVendorById([FromQuery] int? id)
{
var vendor = await _db.Vendors.Where(v => v.Id == id).Select(v => new
{
v.Id,
v.CompanyName,
v.ContactName,
v.Email,
v.Phone,
v.Address,
v.City,
v.State,
v.Zip,
v.TradeSpecialties,
v.IsActive
}).FirstOrDefaultAsync();
if (id == null)
return BadRequest(new Response { Status = "Error", Message = "Id is required" });
var vendor = await _vendorService.GetVendorByIdAsync(id.Value);
if (vendor == null)
return NotFound(new Response { Status = "Error", Message = "Vendor not found" });
@ -92,72 +70,98 @@ namespace Api.SeaHavenIndustries.Controllers
}
[HttpPost]
[HttpPost("Create")]
public async Task<IActionResult> Create([FromBody] Vendor_DTO model)
{
try
{
var vendor = new Vendor
var createDto = new CreateVendorDTO
{
CompanyName = model.CompanyName,
ContactName = model.ContactName,
Email = model.Email,
Phone = model.Phone,
Address = model.Address,
City = model.City,
State = model.State,
Zip = model.Zip,
TradeSpecialties = model.TradeSpecialties,
IsActive = model.IsActive
Name = model.CompanyName ?? throw new ArgumentException("CompanyName is required")
};
_db.Vendors.Add(vendor);
await _db.SaveChangesAsync();
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
await _vendorService.CreateVendorAsync(createDto, userId);
return Ok(new DataResponse { Message = "Vendor Created", Status = "200" });
}
catch (ValidationException vex)
{
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
return BadRequest(new Response { Status = "Validation Error", Message = errors });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
[HttpPut("{id}")]
public async Task<IActionResult> Update(int id, [FromBody] EditVendor_DTO model)
[HttpPost("Update")]
public async Task<IActionResult> Update([FromRoute] int? id, [FromBody] EditVendor_DTO model)
{
try
{
var exist = await _db.Vendors.FindAsync(id);
if (exist == null)
return BadRequest(new Response { Status = "Error", Message = "Vendor not found" });
// For named route, id comes from model
int vendorId = id ?? model.Id;
if (vendorId == 0)
return BadRequest(new Response { Status = "Error", Message = "Vendor Id is required" });
exist.CompanyName = model.CompanyName;
exist.ContactName = model.ContactName;
exist.Email = model.Email;
exist.Phone = model.Phone;
exist.Address = model.Address;
exist.City = model.City;
exist.State = model.State;
exist.Zip = model.Zip;
exist.TradeSpecialties = model.TradeSpecialties;
exist.IsActive = model.IsActive;
var updateDto = new UpdateVendorDTO
{
Name = model.CompanyName
};
await _db.SaveChangesAsync();
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
await _vendorService.UpdateVendorAsync(vendorId, updateDto, userId);
return Ok(new DataResponse { Message = "Vendor Updated", Status = "200" });
}
catch (ValidationException vex)
{
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
return BadRequest(new Response { Status = "Validation Error", Message = errors });
}
catch (KeyNotFoundException)
{
return NotFound(new Response { Status = "Error", Message = "Vendor not found" });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id)
[HttpPost("Delete")]
public async Task<IActionResult> Delete([FromRoute] int? id, [FromQuery] int? queryId = null)
{
var exist = await _db.Vendors.FindAsync(id);
if (exist == null)
return BadRequest(new Response { Status = "Error", Message = "Vendor not found" });
try
{
// Support both route parameter and query string
int vendorId = id ?? queryId ?? 0;
if (vendorId == 0)
return BadRequest(new Response { Status = "Error", Message = "Vendor Id is required" });
_db.Vendors.Remove(exist);
await _db.SaveChangesAsync();
return Ok(new DataResponse { Message = "Vendor Deleted", Status = "200" });
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
await _vendorService.DeleteVendorAsync(vendorId, userId);
return Ok(new DataResponse { Message = "Vendor Deleted", Status = "200" });
}
catch (KeyNotFoundException)
{
return NotFound(new Response { Status = "Error", Message = "Vendor not found" });
}
catch (Exception ex)
{
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
[HttpGet("Dropdown")]

View file

@ -232,10 +232,10 @@ namespace Api.SeaHavenIndustries.Controllers
Location = dispatch.WorkOrder?.Locations == null ? null : new
{
dispatch.WorkOrder.Locations.Name,
dispatch.WorkOrder.Locations.Address,
dispatch.WorkOrder.Locations.Address1,
dispatch.WorkOrder.Locations.City,
dispatch.WorkOrder.Locations.State,
dispatch.WorkOrder.Locations.ZipCode
dispatch.WorkOrder.Locations.Zip
},
Checklist = checklist,
Signoffs = signoffs,

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,7 @@
namespace Api.SeaHavenIndustries.DTOs
using Data.SeaHavenIndustries;
using SeaHaven.Services.DTOs;
namespace Api.SeaHavenIndustries.DTOs
{
public class Contact_DTO
{
@ -20,5 +23,100 @@
public string? TwitterUrl { get; set; }
public int? LocationId { get; set; }
public int? AccountId { get; set; }
// ✅ Map API DTO to Service DTO
public CreateContactDTO ToServiceCreateDTO()
{
return new CreateContactDTO
{
FirstName = this.FirstName,
LastName = this.LastName,
Email = this.Email ?? "",
Phone = this.PhoneNumber,
Title = this.Title,
Type = this.ContactType,
AccountId = this.AccountId
};
}
// Map from Entity to DTO (Model)
public static Contact_DTO MapToModel(Contacts entity)
{
return new Contact_DTO
{
Title = entity.Title,
Owner = entity.Owner,
FirstName = entity.FirstName,
MiddleName = entity.MiddleName,
LastName = entity.LastName,
ContactType = entity.ContactType,
PhoneNumber = entity.PhoneNumber,
Email = entity.Email,
Address1 = entity.Address1,
Address2 = entity.Address2,
City = entity.City,
State = entity.State,
Zip = entity.Zip,
FacebookUrl = entity.FacebookUrl,
LinkedInUrl = entity.LinkedInUrl,
TwitterUrl = entity.TwitterUrl,
LocationId = entity.LocationId,
AccountId = entity.AccountId
};
}
// Map from DTO (Model) to Entity
public static Contacts MapToEntity(Contact_DTO dto, Contacts? existingEntity = null)
{
var entity = existingEntity ?? new Contacts();
entity.Title = dto.Title;
entity.Owner = dto.Owner;
entity.FirstName = dto.FirstName;
entity.MiddleName = dto.MiddleName;
entity.LastName = dto.LastName;
entity.ContactType = dto.ContactType;
entity.PhoneNumber = dto.PhoneNumber;
entity.Email = dto.Email;
entity.Address1 = dto.Address1;
entity.Address2 = dto.Address2;
entity.City = dto.City;
entity.State = dto.State;
entity.Zip = dto.Zip;
entity.FacebookUrl = dto.FacebookUrl;
entity.LinkedInUrl = dto.LinkedInUrl;
entity.TwitterUrl = dto.TwitterUrl;
entity.LocationId = dto.LocationId;
entity.AccountId = dto.AccountId;
return entity;
}
}
// DTO for contact list view
public class ContactListItem_DTO
{
public int Id { get; set; }
public string? Name { get; set; }
public string? Email { get; set; }
public string? Phone { get; set; }
// ✅ Map from Service DTO
public static ContactListItem_DTO FromServiceDTO(ContactDTO serviceDto)
{
return new ContactListItem_DTO
{
Id = serviceDto.Id,
Name = (serviceDto.FirstName + " " + serviceDto.LastName).Trim(),
Email = serviceDto.Email,
Phone = serviceDto.Phone
};
}
// ✅ Map collection from Service DTOs
public static IEnumerable<ContactListItem_DTO> FromServiceDTOs(IEnumerable<ContactDTO> serviceDtos)
{
return serviceDtos.Select(FromServiceDTO);
}
}
}

View file

@ -1,4 +1,7 @@
namespace Api.SeaHavenIndustries.DTOs
using Data.SeaHavenIndustries;
using SeaHaven.Services.DTOs;
namespace Api.SeaHavenIndustries.DTOs
{
public class EditContact_DTO
{
@ -21,5 +24,72 @@
public string? TwitterUrl { get; set; }
public int? LocationId { get; set; }
public int? AccountId { get; set; }
// ✅ Map API DTO to Service DTO
public UpdateContactDTO ToServiceUpdateDTO()
{
return new UpdateContactDTO
{
FirstName = this.FirstName,
LastName = this.LastName,
Email = this.Email,
Phone = this.PhoneNumber,
Title = this.Title,
Type = this.ContactType,
AccountId = this.AccountId
};
}
// Map from Entity to DTO (Model)
public static EditContact_DTO MapToModel(Contacts entity)
{
return new EditContact_DTO
{
Id = entity.Id,
Title = entity.Title,
Owner = entity.Owner,
FirstName = entity.FirstName,
MiddleName = entity.MiddleName,
LastName = entity.LastName,
ContactType = entity.ContactType,
PhoneNumber = entity.PhoneNumber,
Email = entity.Email,
Address1 = entity.Address1,
Address2 = entity.Address2,
City = entity.City,
State = entity.State,
Zip = entity.Zip,
FacebookUrl = entity.FacebookUrl,
LinkedInUrl = entity.LinkedInUrl,
TwitterUrl = entity.TwitterUrl,
LocationId = entity.LocationId,
AccountId = entity.AccountId
};
}
// Map from DTO (Model) to Entity - updates existing entity
public static Contacts MapToEntity(EditContact_DTO dto, Contacts existingEntity)
{
existingEntity.Title = dto.Title;
existingEntity.Owner = dto.Owner;
existingEntity.FirstName = dto.FirstName;
existingEntity.MiddleName = dto.MiddleName;
existingEntity.LastName = dto.LastName;
existingEntity.ContactType = dto.ContactType;
existingEntity.PhoneNumber = dto.PhoneNumber;
existingEntity.Email = dto.Email;
existingEntity.Address1 = dto.Address1;
existingEntity.Address2 = dto.Address2;
existingEntity.City = dto.City;
existingEntity.State = dto.State;
existingEntity.Zip = dto.Zip;
existingEntity.FacebookUrl = dto.FacebookUrl;
existingEntity.LinkedInUrl = dto.LinkedInUrl;
existingEntity.TwitterUrl = dto.TwitterUrl;
existingEntity.LocationId = dto.LocationId;
existingEntity.AccountId = dto.AccountId;
return existingEntity;
}
}
}

View 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;
}
}

View file

@ -1,3 +1,5 @@
using SeaHaven.Services.DTOs;
namespace Api.SeaHavenIndustries.DTOs
{
public class EditLocation_DTO
@ -17,5 +19,19 @@ namespace Api.SeaHavenIndustries.DTOs
public int? GetAccountId() =>
int.TryParse(AccountId, out var id) ? id : null;
// ✅ Map API DTO to Service DTO
public UpdateLocationDTO ToServiceUpdateDTO()
{
return new UpdateLocationDTO
{
LocationName = this.Name,
Address = this.Address,
City = this.City,
State = this.State,
Zipcode = this.ZipCode,
AccountId = this.GetAccountId()
};
}
}
}

View 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; }
}
}

View file

@ -1,3 +1,6 @@
using Data.SeaHavenIndustries;
using SeaHaven.Services.DTOs;
namespace Api.SeaHavenIndustries.DTOs
{
public class Location_DTO
@ -16,5 +19,117 @@ namespace Api.SeaHavenIndustries.DTOs
public int? GetAccountId() =>
int.TryParse(AccountId, out var id) ? id : null;
// ✅ Map API DTO to Service DTO
public CreateLocationDTO ToServiceCreateDTO()
{
return new CreateLocationDTO
{
LocationName = this.Name ?? "",
Address = this.Address,
City = this.City,
State = this.State,
Zipcode = this.ZipCode,
AccountId = this.GetAccountId()
};
}
// Map from Entity to DTO
public static Location_DTO MapToModel(Locations entity)
{
return new Location_DTO
{
Title = entity.Title,
Name = entity.Name,
Address = entity.Address1,
City = entity.City,
State = entity.State,
ZipCode = entity.Zip,
Phone = entity.PhoneNumber,
Contact = null, // Not in database schema
ContactEmail = entity.Email,
Status = entity.Status,
AccountId = null // Not in database schema
};
}
// Map from DTO to Entity
public static Locations MapToEntity(Location_DTO dto, Locations? existingEntity = null)
{
var entity = existingEntity ?? new Locations();
entity.Title = dto.Title;
entity.Name = dto.Name;
entity.Address1 = dto.Address;
entity.City = dto.City;
entity.State = dto.State;
entity.Zip = dto.ZipCode;
entity.PhoneNumber = dto.Phone;
// Contact field doesn't exist in database schema - ignored
entity.Email = dto.ContactEmail;
entity.Status = dto.Status;
// AccountId field doesn't exist in database schema - ignored
return entity;
}
}
// DTO for list views
public class LocationListItem_DTO
{
public int Id { get; set; }
public string? Name { get; set; }
public string? Title { get; set; }
// Map from Entity
public static LocationListItem_DTO MapToModel(Locations entity)
{
return new LocationListItem_DTO
{
Id = entity.Id,
Name = entity.Name,
Title = entity.Title
};
}
// ✅ Map from Service DTO (no logic in controller)
public static LocationListItem_DTO FromServiceDTO(LocationDTO serviceDto)
{
return new LocationListItem_DTO
{
Id = serviceDto.Id,
Name = serviceDto.Name ?? serviceDto.Title,
Title = serviceDto.Title ?? serviceDto.Name
};
}
// ✅ Map collection from Service DTOs
public static IEnumerable<LocationListItem_DTO> FromServiceDTOs(IEnumerable<LocationDTO> serviceDtos)
{
return serviceDtos.Select(FromServiceDTO);
}
}
// DTO for location dropdown/simple view
public class LocationViewModel_DTO
{
public int? Id { get; set; }
public string? Name { get; set; }
// ✅ Map from Service DTO
public static LocationViewModel_DTO FromServiceDTO(LocationDTO serviceDto)
{
return new LocationViewModel_DTO
{
Id = serviceDto.Id,
Name = serviceDto.Name ?? serviceDto.Title
};
}
// ✅ Map collection from Service DTOs
public static IEnumerable<LocationViewModel_DTO> FromServiceDTOs(IEnumerable<LocationDTO> serviceDtos)
{
return serviceDtos.Select(FromServiceDTO);
}
}
}

View file

@ -1,4 +1,5 @@
using Data.SeaHavenIndustries;
using Data.SeaHavenIndustries.Enums;
namespace Api.SeaHavenIndustries.DTOs
{
@ -9,14 +10,11 @@ namespace Api.SeaHavenIndustries.DTOs
public string? Description { get; set; }
public string? PO { get; set; }
public string? TT { get; set; }
//public int? ContactId { get; set; }
public string? AssignTo { get; set; }
public DateTime? DueDate { get; set; }
public string? Priority { get; set; }
public WorkOrderPriority? Priority { get; set; }
public int? LocationId { get; set; }
public bool? istemplate { get; set; }
//public int? CategoryId { get; set; }
public List<IFormFile>? Attachments { get; set; }
public string? BeforPhotoIssue { get; set; }
public IFormFile? BeforPhotoAttachment { get; set; }
@ -25,15 +23,33 @@ namespace Api.SeaHavenIndustries.DTOs
public string? SignOffName { get; set; }
public IFormFile? SignOffAttachment { get; set; }
public string? SignOffSignature { get; set; }
public string? WorkOrderContacts { get; set; }
public string? WorkOrderCategories { get; set; }
public List<int> GetContactIds()
public List<int>? ContactIds { get; set; }
public List<int>? CategoryIds { get; set; }
public static WorkOrder MapToEntity(Workorder_DTO dto, string userId)
{
return WorkOrderContacts?.Split(',').Select(int.Parse).ToList() ?? new List<int>();
}
public List<int> GetCategoryIds()
{
return WorkOrderCategories?.Split(',').Select(int.Parse).ToList() ?? new List<int>();
if (dto == null) return null!;
return new WorkOrder
{
WorkerOrderNumber = dto.WorkerOrderNumber,
WorkerOrderTitle = dto.WorkerOrderTitle,
Description = dto.Description,
PO = dto.PO,
TT = dto.TT,
AssignTo = dto.AssignTo,
DueDate = dto.DueDate,
Priority = (dto.Priority ?? WorkOrderPriority.Medium).ToString(),
LocationId = dto.LocationId,
istemplate = dto.istemplate ?? false,
Status = WorkOrderStatus.Open.ToString(),
BeforPhotoIssue = dto.BeforPhotoIssue,
AfterPhotoIssue = dto.AfterPhotoIssue,
SignOffName = dto.SignOffName,
SignOffSignature = dto.SignOffSignature,
createdby = userId,
CreatedDate = DateTime.UtcNow
};
}
}
public class EditWorkorder_DTO
@ -46,8 +62,8 @@ namespace Api.SeaHavenIndustries.DTOs
public string? TT { get; set; }
public string? AssignTo { get; set; }
public DateTime? DueDate { get; set; }
public string? Priority { get; set; }
public string? Status { get; set; }
public WorkOrderPriority? Priority { get; set; }
public WorkOrderStatus? Status { get; set; }
public int? LocationId { get; set; }
public string? Problem { get; set; }
public string? Trade { get; set; }
@ -56,7 +72,6 @@ namespace Api.SeaHavenIndustries.DTOs
public DateTime? ScheduledDate { get; set; }
public DateTime? CompletedDate { get; set; }
public string? Source { get; set; }
public List<IFormFile>? Attachments { get; set; }
public string? BeforPhotoIssue { get; set; }
public IFormFile? BeforPhotoAttachment { get; set; }
@ -65,16 +80,36 @@ namespace Api.SeaHavenIndustries.DTOs
public string? SignOffName { get; set; }
public IFormFile? SignOffAttachment { get; set; }
public string? SignOffSignature { get; set; }
public string? WorkOrderContacts { get; set; }
public string? WorkOrderCategories { get; set; }
public List<int> GetContactIds()
public List<int>? ContactIds { get; set; }
public List<int>? CategoryIds { get; set; }
public static void UpdateEntity(EditWorkorder_DTO dto, WorkOrder entity)
{
return WorkOrderContacts?.Split(',').Select(int.Parse).ToList() ?? new List<int>();
}
public List<int> GetCategoryIds()
{
return WorkOrderCategories?.Split(',').Select(int.Parse).ToList() ?? new List<int>();
if (dto == null || entity == null) return;
if (dto.WorkerOrderNumber != null) entity.WorkerOrderNumber = dto.WorkerOrderNumber;
if (dto.WorkerOrderTitle != null) entity.WorkerOrderTitle = dto.WorkerOrderTitle;
if (dto.Description != null) entity.Description = dto.Description;
if (dto.PO != null) entity.PO = dto.PO;
if (dto.TT != null) entity.TT = dto.TT;
if (dto.AssignTo != null) entity.AssignTo = dto.AssignTo;
if (dto.DueDate.HasValue) entity.DueDate = dto.DueDate;
if (dto.Priority.HasValue) entity.Priority = dto.Priority.ToString()!;
if (dto.Status.HasValue) entity.Status = dto.Status.ToString()!;
if (dto.LocationId.HasValue) entity.LocationId = dto.LocationId;
if (dto.Problem != null) entity.Problem = dto.Problem;
if (dto.Trade != null) entity.Trade = dto.Trade;
if (dto.SubTrade != null) entity.SubTrade = dto.SubTrade;
if (dto.VendorNTE.HasValue) entity.VendorNTE = dto.VendorNTE;
if (dto.ScheduledDate.HasValue) entity.ScheduledDate = dto.ScheduledDate;
if (dto.CompletedDate.HasValue) entity.CompletedDate = dto.CompletedDate;
if (dto.Source != null) entity.Source = dto.Source;
if (dto.BeforPhotoIssue != null) entity.BeforPhotoIssue = dto.BeforPhotoIssue;
if (dto.AfterPhotoIssue != null) entity.AfterPhotoIssue = dto.AfterPhotoIssue;
if (dto.SignOffName != null) entity.SignOffName = dto.SignOffName;
if (dto.SignOffSignature != null) entity.SignOffSignature = dto.SignOffSignature;
entity.LastModificationTime = DateTime.UtcNow;
}
}
}

View file

@ -27,19 +27,19 @@ namespace Api.SeaHavenIndustries.Helper
//mail.Body = body;
//mail.IsBodyHtml = true;
//SmtpClient smtp = new SmtpClient("mail.codevoir.com");
//NetworkCredential Credentials = new NetworkCredential("infoesquall@codevoir.com", "Abc123!@#");
//NetworkCredential Credentials = new NetworkCredential("<smtp-user>", "<smtp-password>");
//smtp.UseDefaultCredentials = false;
//smtp.Credentials = Credentials;
//smtp.Port = 8889; //25 alternative port number is 8889
//smtp.EnableSsl = false;
//smtp.Send(mail);
//var apiKey = Environment.GetEnvironmentVariable("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\r\n");
//var apiKey1 = Environment.GetEnvironmentVariable("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");
//var apiKey = Environment.GetEnvironmentVariable("<SENDGRID_API_KEY>\r\n");
//var apiKey1 = Environment.GetEnvironmentVariable("<SENDGRID_API_KEY>");
// var apiKey2 = Environment.GetEnvironmentVariable("SENDGRID_API_KEY");
var apiKey = _configuration.GetValue<string>("SendGrid:ApiKey");
//var apiKey = "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";
//var apiKey = "<SENDGRID_API_KEY>";
var client = new SendGridClient(apiKey);
var from = new EmailAddress("tech@seahavenind.com", "Sea haven Industries");
//var subject = "Sending with SendGrid is Fun";
@ -65,7 +65,7 @@ namespace Api.SeaHavenIndustries.Helper
var apiKey = _configuration.GetValue<string>("SendGrid:ApiKey");
//var apiKey = "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";
//var apiKey = "<SENDGRID_API_KEY>";
var client = new SendGridClient(apiKey);
var from = new EmailAddress("tech@seahavenind.com", "Sea haven Industries");
//var subject = "Sending with SendGrid is Fun";

View file

@ -7,6 +7,8 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using System.Text;
using SeaHaven.DataServices.DependencyInjection;
using SeaHaven.Services.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
ConfigurationManager configuration = builder.Configuration;
@ -31,6 +33,7 @@ builder.Services.AddResponseCompression(opts =>
});
builder.Services.AddCors(option =>
option.AddDefaultPolicy(builder => builder.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod()));
builder.Services.AddScoped<SendMessage>();
builder.Services.AddScoped<Api.SeaHavenIndustries.Helper.VendorPortalTokenService>();
builder.Services.AddSingleton<Api.SeaHavenIndustries.Helper.ZipCodeDistance>(sp =>
@ -41,6 +44,10 @@ builder.Services.AddSingleton<Api.SeaHavenIndustries.Helper.ZipCodeDistance>(sp
if (File.Exists(csvPath)) zipService.Load(csvPath);
return zipService;
});
builder.Services.AddDataServices();
builder.Services.AddBusinessServices();
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
@ -111,60 +118,60 @@ app.UseAuthorization();
app.MapControllers();
if (app.Environment.IsDevelopment())
{
using var scope = app.Services.CreateScope();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();
//if (app.Environment.IsDevelopment())
//{
// using var scope = app.Services.CreateScope();
// var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
// var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();
if (!await roleManager.RoleExistsAsync("Admin"))
await roleManager.CreateAsync(new IdentityRole("Admin"));
if (!await roleManager.RoleExistsAsync("Dispatcher"))
await roleManager.CreateAsync(new IdentityRole("Dispatcher"));
if (!await roleManager.RoleExistsAsync("Manager"))
await roleManager.CreateAsync(new IdentityRole("Manager"));
// if (!await roleManager.RoleExistsAsync("Admin"))
// await roleManager.CreateAsync(new IdentityRole("Admin"));
// if (!await roleManager.RoleExistsAsync("Dispatcher"))
// await roleManager.CreateAsync(new IdentityRole("Dispatcher"));
// if (!await roleManager.RoleExistsAsync("Manager"))
// await roleManager.CreateAsync(new IdentityRole("Manager"));
var adminEmail = "admin@seahavenind.com";
if (await userManager.FindByNameAsync(adminEmail) == null)
{
var admin = new ApplicationUser
{
UserName = adminEmail,
Email = adminEmail,
FirstName = "Admin",
LastName = "User",
EmailConfirmed = true,
UniqueName = "Active",
CreatedDate = DateTime.Now
};
var result = await userManager.CreateAsync(admin, "Admin@123");
if (result.Succeeded)
await userManager.AddToRoleAsync(admin, "Admin");
}
// var adminEmail = "admin@seahavenind.com";
// if (await userManager.FindByNameAsync(adminEmail) == null)
// {
// var admin = new ApplicationUser
// {
// UserName = adminEmail,
// Email = adminEmail,
// FirstName = "Admin",
// LastName = "User",
// EmailConfirmed = true,
// UniqueName = "Active",
// CreatedDate = DateTime.Now
// };
// var result = await userManager.CreateAsync(admin, "Admin@123");
// if (result.Succeeded)
// await userManager.AddToRoleAsync(admin, "Admin");
// }
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
if (!db.DropdownOptions.Any())
{
var trades = new[] { "Plumbing", "Electrical", "HVAC", "Fire/Life Safety", "General", "Roofing", "Janitorial", "Painting", "Locksmith", "Carpentry" };
for (int i = 0; i < trades.Length; i++)
db.DropdownOptions.Add(new DropdownOption { Category = "Trade", Value = trades[i], SortOrder = i });
// var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// if (!db.DropdownOptions.Any())
// {
// var trades = new[] { "Plumbing", "Electrical", "HVAC", "Fire/Life Safety", "General", "Roofing", "Janitorial", "Painting", "Locksmith", "Carpentry" };
// for (int i = 0; i < trades.Length; i++)
// db.DropdownOptions.Add(new DropdownOption { Category = "Trade", Value = trades[i], SortOrder = i });
var subTrades = new (string trade, string sub)[] {
("Plumbing", "Drain Cleaning"), ("Plumbing", "Pipe Repair"), ("Plumbing", "Fixture Install"), ("Plumbing", "Backflow Testing"),
("Electrical", "Lighting"), ("Electrical", "Panel/Breaker"), ("Electrical", "Wiring"), ("Electrical", "Generator"),
("HVAC", "AC Repair"), ("HVAC", "Heating"), ("HVAC", "Ductwork"), ("HVAC", "Controls/BMS"),
("Fire/Life Safety", "Sprinkler"), ("Fire/Life Safety", "Fire Alarm"), ("Fire/Life Safety", "Extinguisher"), ("Fire/Life Safety", "Fire Pump"),
("General", "Doors"), ("General", "Flooring"), ("General", "Drywall"), ("General", "Concrete"),
};
for (int i = 0; i < subTrades.Length; i++)
db.DropdownOptions.Add(new DropdownOption { Category = "SubTrade", Value = subTrades[i].sub, ParentValue = subTrades[i].trade, SortOrder = i });
// var subTrades = new (string trade, string sub)[] {
// ("Plumbing", "Drain Cleaning"), ("Plumbing", "Pipe Repair"), ("Plumbing", "Fixture Install"), ("Plumbing", "Backflow Testing"),
// ("Electrical", "Lighting"), ("Electrical", "Panel/Breaker"), ("Electrical", "Wiring"), ("Electrical", "Generator"),
// ("HVAC", "AC Repair"), ("HVAC", "Heating"), ("HVAC", "Ductwork"), ("HVAC", "Controls/BMS"),
// ("Fire/Life Safety", "Sprinkler"), ("Fire/Life Safety", "Fire Alarm"), ("Fire/Life Safety", "Extinguisher"), ("Fire/Life Safety", "Fire Pump"),
// ("General", "Doors"), ("General", "Flooring"), ("General", "Drywall"), ("General", "Concrete"),
// };
// for (int i = 0; i < subTrades.Length; i++)
// db.DropdownOptions.Add(new DropdownOption { Category = "SubTrade", Value = subTrades[i].sub, ParentValue = subTrades[i].trade, SortOrder = i });
var problems = new[] { "Leak", "No Power", "Not Cooling", "Not Heating", "Clogged", "Broken/Damaged", "Alarm Fault", "Preventive Maintenance", "Inspection", "Installation", "Other" };
for (int i = 0; i < problems.Length; i++)
db.DropdownOptions.Add(new DropdownOption { Category = "Problem", Value = problems[i], SortOrder = i });
// var problems = new[] { "Leak", "No Power", "Not Cooling", "Not Heating", "Clogged", "Broken/Damaged", "Alarm Fault", "Preventive Maintenance", "Inspection", "Installation", "Other" };
// for (int i = 0; i < problems.Length; i++)
// db.DropdownOptions.Add(new DropdownOption { Category = "Problem", Value = problems[i], SortOrder = i });
await db.SaveChangesAsync();
}
}
// await db.SaveChangesAsync();
// }
//}
app.Run();

View file

@ -1,6 +1,11 @@
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost,1433;Database=SeahavenIndustries;User Id=sa;Password=Local@Dev2026!;TrustServerCertificate=True;MultipleActiveResultSets=true"
//"DefaultConnection": "Server=DESKTOP-64LC14O\SQLEXPRESS;Database=SeahavenIndustries;TrustServerCertificate=True;Trusted_Connection=True;MultipleActiveResultSets=true",
// Provide the real value via environment variable ConnectionStrings__DefaultConnection or user-secrets.
// Do NOT commit credentials. This file is committed, so keep only the placeholder here.
"DefaultConnection": "${CONNECTION_STRING}"
},
"Logging": {
"LogLevel": {
@ -9,20 +14,14 @@
}
},
"SendGrid": {
"ApiKey": "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"
// Provide the real value via environment variable SendGrid__ApiKey or user-secrets.
"ApiKey": "${SENDGRID_API_KEY}"
},
"AllowedHosts": "*",
"JWT": {
"ValidAudience": "http://localhost:3000",
"ValidIssuer": "https://localhost:7195",
"Secret": "JWTAuthenticationHIGHsecuredPasswordVVVp1OH7Xzyr"
},
"FrontendBaseUrl": "http://localhost:3000",
"VendorPortal": {
"TokenLifetimeDays": 365
},
"Approvals": {
"UpliftTier1MaxUsd": 2500,
"Tier1Roles": [ "Admin", "Dispatcher" ],
"Tier2Roles": [ "Admin", "Manager" ]
"ValidAudience": "http://localhost:4200",
"ValidIssuer": "http://localhost:7195",
// Provide the real value via environment variable JWT__Secret or user-secrets.
"Secret": "${JWT_SECRET}"
}
}
}

View file

@ -0,0 +1,24 @@
{
"ConnectionStrings": {
// Provide the real value via environment variable ConnectionStrings__DefaultConnection
// (e.g. Elastic Beanstalk environment property) or the instance secret store. Do NOT commit credentials.
"DefaultConnection": "${CONNECTION_STRING}"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"SendGrid": {
// Provide the real value via environment variable SendGrid__ApiKey or the instance secret store.
"ApiKey": "${SENDGRID_API_KEY}"
},
"AllowedHosts": "*",
"JWT": {
"ValidAudience": "http://console.seahavenind.com",
"ValidIssuer": "http://console.seahavenind.com",
// Provide the real value via environment variable JWT__Secret or the instance secret store.
"Secret": "${JWT_SECRET}"
}
}

View file

@ -1,6 +1,11 @@
{
"ConnectionStrings": {
"DefaultConnection": ""
//"DefaultConnection": "Server=DESKTOP-64LC14O\SQLEXPRESS;Database=SeahavenIndustries;TrustServerCertificate=True;Trusted_Connection=True;MultipleActiveResultSets=true",
// Provide the real value via environment variable ConnectionStrings__DefaultConnection,
// user-secrets, or appsettings.Development.json (which is gitignored). Do NOT commit credentials.
"DefaultConnection": "${CONNECTION_STRING}"
},
"Logging": {
"LogLevel": {
@ -9,17 +14,14 @@
}
},
"SendGrid": {
"ApiKey": ""
// Provide the real value via environment variable SendGrid__ApiKey or user-secrets.
"ApiKey": "${SENDGRID_API_KEY}"
},
"AllowedHosts": "*",
"JWT": {
"ValidAudience": "http://localhost:4200",
"ValidIssuer": "http://localhost:7195",
"Secret": ""
},
"Approvals": {
"UpliftTier1MaxUsd": 2500,
"Tier1Roles": [ "Admin", "Dispatcher" ],
"Tier2Roles": [ "Admin", "Manager" ]
// Provide the real value via environment variable JWT__Secret or user-secrets.
"Secret": "${JWT_SECRET}"
}
}
}

View file

@ -1,13 +1,12 @@
{
"region" : "us-east-1",
"configuration" : "Debug",
"framework" : "net8.0",
"self-contained" : true,
"application" : "SeaHavenIndustries",
"environment" : "SeaHavenIndustries-dev",
"enable-xray" : false,
"enhanced-health-type" : "basic",
"additional-options" : "",
"proxy-server" : "nginx"
"region": "us-east-1",
"configuration": "Debug",
"framework": "net8.0",
"self-contained": true,
"application": "SeaHavenIndustries",
"environment": "SeaHavenIndustries-dev",
"enable-xray": false,
"enhanced-health-type": "basic",
"additional-options": "",
"proxy-server": "nginx"
}

View file

@ -924,6 +924,206 @@ PRIMARY KEY CLUSTERED
GO
-- ==================================================================
-- DATABASE CHANGES APPLIED ON 2026-04-22
-- Migration: Calendar Module + Missing Column Fixes
-- Applied by: AI Agent
-- ==================================================================
-- ==================================================================
-- 1. EVENTS TABLE (Calendar Module)
-- Migration: 20260422021353_AddEventsTable
-- ==================================================================
USE [db_a7141e_seahavenindustry]
GO
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Events]') AND type in (N'U'))
BEGIN
CREATE TABLE [dbo].[Events](
[Id] [int] IDENTITY(1,1) NOT NULL,
-- Basic Event Information
[Title] [nvarchar](100) NOT NULL,
[Description] [nvarchar](max) NULL,
[Location] [nvarchar](150) NULL,
-- Related Record (Link to Accounts, Contacts, Work Orders, etc.)
[RecordID] [int] NULL,
[RecordName] [nvarchar](max) NULL,
[RecordTargetType] [nvarchar](max) NULL,
-- Reminder & Display Settings
[ReminderMinutes] [int] NOT NULL CONSTRAINT [DF_Events_ReminderMinutes] DEFAULT ((15)),
[EventColor] [nvarchar](20) NOT NULL CONSTRAINT [DF_Events_EventColor] DEFAULT (N'primary'),
-- Schedule Information
[StartDate] [datetime2](7) NOT NULL,
[StartTime] [nvarchar](10) NULL,
[EndDate] [datetime2](7) NOT NULL,
[EndTime] [nvarchar](10) NULL,
[AllDay] [bit] NOT NULL CONSTRAINT [DF_Events_AllDay] DEFAULT ((0)),
-- Audit Fields (from FullAuditEntity)
[IsDeleted] [bit] NOT NULL CONSTRAINT [DF_Events_IsDeleted] DEFAULT ((0)),
[DeletionTime] [datetime2](7) NULL,
[createdby] [nvarchar](max) NULL,
[CreatedDate] [datetime2](7) NULL,
[lastmodifiedby] [nvarchar](max) NULL,
[LastModificationTime] [datetime2](7) NULL,
CONSTRAINT [PK_Events] PRIMARY KEY CLUSTERED ([Id] ASC)
WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
END
GO
-- Create Index on StartDate for faster calendar queries
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Events_StartDate' AND object_id = OBJECT_ID('[dbo].[Events]'))
BEGIN
CREATE NONCLUSTERED INDEX [IX_Events_StartDate] ON [dbo].[Events]
(
[StartDate] ASC
)
WHERE ([IsDeleted] = 0)
END
GO
-- Create Index on IsDeleted for soft delete queries
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Events_IsDeleted' AND object_id = OBJECT_ID('[dbo].[Events]'))
BEGIN
CREATE NONCLUSTERED INDEX [IX_Events_IsDeleted] ON [dbo].[Events]
(
[IsDeleted] ASC
)
END
GO
-- ==================================================================
-- 2. WORKORDERS TABLE - MISSING COLUMNS
-- Migrations: 20260416212330, 20260416224015, 20260417142453
-- ==================================================================
-- Migration: 20260416224015_AddInternalWONumber
ALTER TABLE [dbo].[WorkOrders]
ADD [InternalWONumber] [nvarchar](max) NULL
GO
-- Migration: 20260416212330_AddExternalSyncFields
ALTER TABLE [dbo].[WorkOrders]
ADD [ExternalWorkOrderId] [nvarchar](max) NULL,
[Source] [nvarchar](max) NULL
GO
-- Migration: 20260417142453_AddDropdownOptionsAndWOFields
ALTER TABLE [dbo].[WorkOrders]
ADD [Problem] [nvarchar](max) NULL,
[Trade] [nvarchar](max) NULL,
[SubTrade] [nvarchar](max) NULL,
[VendorNTE] [decimal](18, 2) NULL,
[ScheduledDate] [datetime2](7) NULL,
[CompletedDate] [datetime2](7) NULL
GO
-- ==================================================================
-- 3. COMMENTS TABLE - MISSING COLUMNS
-- Migrations: 20260416220656, 20260417171152
-- ==================================================================
-- Migration: 20260416220656_AddCommentTypeAndAuditLog
ALTER TABLE [dbo].[Comments]
ADD [CommentType] [nvarchar](max) NULL,
[Commenter] [nvarchar](max) NULL,
[RecordType] [nvarchar](max) NULL
GO
-- Migration: 20260417171152_AddDispatchNumberAndCommentDispatchId
ALTER TABLE [dbo].[Comments]
ADD [DispatchId] [int] NULL
GO
-- ==================================================================
-- 4. LOCATIONS TABLE - MISSING COLUMNS
-- Migration: 20260416210833_AddLocationFields
-- ==================================================================
ALTER TABLE [dbo].[Locations]
ADD [Address1] [nvarchar](max) NULL,
[Address2] [nvarchar](max) NULL,
[City] [nvarchar](max) NULL,
[State] [nvarchar](max) NULL,
[Zip] [nvarchar](max) NULL,
[PhoneNumber] [nvarchar](max) NULL,
[Email] [nvarchar](max) NULL,
[Status] [nvarchar](max) NULL
GO
-- ==================================================================
-- 5. MIGRATION HISTORY RECORDS
-- Marked these migrations as applied to sync with code
-- ==================================================================
INSERT INTO [dbo].[__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES
(N'20260416204109_SyncContactsAndModels', N'8.0.8'),
(N'20260416210833_AddLocationFields', N'8.0.8'),
(N'20260416212330_AddExternalSyncFields', N'8.0.8'),
(N'20260416220656_AddCommentTypeAndAuditLog', N'8.0.8'),
(N'20260416224015_AddInternalWONumber', N'8.0.8'),
(N'20260417142453_AddDropdownOptionsAndWOFields', N'8.0.8'),
(N'20260417153405_AddVendorAndDispatch', N'8.0.8'),
(N'20260417154641_AddDispatchScheduledDate', N'8.0.8'),
(N'20260417171152_AddDispatchNumberAndCommentDispatchId', N'8.0.8'),
(N'20260417182910_AddTaskListTemplates', N'8.0.8'),
(N'20260417190743_MultiWODispatch', N'8.0.8'),
(N'20260417192012_AddDispatchChecklist', N'8.0.8'),
(N'20260417194438_AddDispatchSignoffs', N'8.0.8'),
(N'20260417195316_AddDispatchVerification', N'8.0.8'),
(N'20260420151445_AddVendorAccessToken', N'8.0.8'),
(N'20260420164708_AddDispatchUpliftRequest', N'8.0.8'),
(N'20260422021353_AddEventsTable', N'8.0.8')
GO
-- ==================================================================
-- SUMMARY OF CHANGES (2026-04-22)
-- ==================================================================
-- Tables Created (11):
-- 1. Events (Calendar/Events module)
-- 2. WorkOrderAuditLogs (Audit trail for work order changes)
-- 3. DropdownOptions (Dynamic dropdown values)
-- 4. Vendors (Vendor management)
-- 5. Dispatches (Vendor dispatch workflow)
-- 6. TaskListTemplates (Reusable task templates)
-- 7. TaskListTemplateItems (Template task items)
-- 8. DispatchWorkOrders (Many-to-many: Dispatch <-> WorkOrders)
-- 9. DispatchChecklistItems (Dispatch checklist)
-- 10. DispatchSignoffs (Customer & Vendor signatures)
-- 11. DispatchUpliftRequests (NTE increase requests)
--
-- Columns Added to WorkOrders (9):
-- - InternalWONumber, ExternalWorkOrderId, Source
-- - Problem, Trade, SubTrade
-- - VendorNTE, ScheduledDate, CompletedDate
--
-- Columns Added to Comments (4):
-- - CommentType, Commenter, RecordType, DispatchId
--
-- Columns Added to Locations (8):
-- - Address1, Address2, City, State, Zip
-- - PhoneNumber, Email, Status
--
-- Columns Added to Dispatches (7):
-- - ScheduledDate, DispatchNumber, CompletedDate
-- - VerifiedBy, VerifiedAt, AcceptToken, AcknowledgedAt
--
-- Columns Added to Vendors (1):
-- - AccessToken
--
-- Migrations Applied: 17
-- From: 20260416204109_SyncContactsAndModels
-- To: 20260422021353_AddEventsTable
--
-- Database Status: UP TO DATE ✓
-- ==================================================================

564
BACKEND_ARCHITECTURE.md Normal file
View file

@ -0,0 +1,564 @@
# Backend Architecture Guide
## 🏗️ Clean Architecture Overview
The backend follows **Clean Architecture** with strict layer separation:
```
┌─────────────────────────────────────────────────────────────┐
│ API LAYER (Controllers) │
│ - HTTP Request/Response │
│ - Authentication & Authorization │
│ - Exception Handling │
│ - Input Validation (basic) │
└─────────────────┬───────────────────────────────────────────┘
│ Calls
▼
┌─────────────────────────────────────────────────────────────┐
│ SERVICE LAYER (Business Logic) │
│ - Business Rules │
│ - FluentValidation (detailed validation) │
│ - Data Transformation │
│ - Orchestration │
└─────────────────┬───────────────────────────────────────────┘
│ Calls
▼
┌─────────────────────────────────────────────────────────────┐
│ DATA SERVICE LAYER (Persistence) │
│ - Database Access │
│ - Entity Operations (CRUD) │
│ - Query Building │
│ - Transaction Management │
└─────────────────┬───────────────────────────────────────────┘
│ Uses
▼
┌─────────────────────────────────────────────────────────────┐
│ DATA LAYER (EF Core) │
│ - DbContext │
│ - Entity Models │
│ - Database Schema │
└─────────────────────────────────────────────────────────────┘
```
---
## 📁 Project Structure
```
backend/shoc-backend/
│
├── Api.SeaHavenIndustries/ # 🌐 API Layer
│ ├── Controllers/ # HTTP endpoints
│ ├── DTOs/ # Data Transfer Objects (API contracts)
│ ├── Program.cs # App startup & DI registration
│ └── appsettings.json # Configuration
│
├── SeaHaven.Services/ # 💼 Service Layer (Business Logic)
│ ├── Interfaces/ # Service contracts (IContactService, etc.)
│ ├── Implementation/ # Service implementations
│ ├── DTOs/ # Internal DTOs for service layer
│ └── Validators/ # FluentValidation validators
│
├── SeaHaven.DataServices/ # 💾 Data Access Layer
│ ├── Interfaces/ # DataService contracts (IContactDataService, etc.)
│ ├── Implementation/ # DataService implementations
│ └── DTOs/ # Data layer DTOs
│
└── Data.SeaHavenIndustries/ # 🗄️ Data Layer (EF Core)
├── Models/ # Entity models (Contact, Employee, etc.)
├── ApplicationDbContext.cs # EF Core DbContext
└── Migrations/ # Database migrations
```
---
## 🔄 Data Flow Example (Creating a Contact)
### 1️⃣ **API Layer** (`ContactController.cs`)
```csharp
[HttpPost("Create")]
public async Task<IActionResult> AddContacts(Contact_DTO dto)
{
try
{
// Get authenticated user ID
var userId = _userManager.GetUserId(User);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
// Convert API DTO → Service DTO
var serviceDto = dto.ToServiceCreateDTO();
// Call Service Layer
await _contactService.CreateContactAsync(serviceDto, userId);
return Ok(new Response { Status = "Success", Message = "Contact created" });
}
catch (ValidationException vex)
{
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
return BadRequest(new Response { Status = "Validation Error", Message = errors });
}
catch (KeyNotFoundException)
{
return NotFound(new Response { Status = "Error", Message = "Resource not found" });
}
catch (Exception ex)
{
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
```
**Responsibilities:**
- ✅ Authenticate user
- ✅ Extract user ID from claims
- ✅ Map API DTO to Service DTO
- ✅ Catch exceptions and return proper HTTP status codes
- ❌ NO business logic
- ❌ NO database access
---
### 2️⃣ **Service Layer** (`ContactService.cs`)
```csharp
public class ContactService : IContactService
{
private readonly IContactDataService _dataService;
private readonly IValidator<CreateContactDTO> _createValidator;
public ContactService(
IContactDataService dataService,
IValidator<CreateContactDTO> createValidator)
{
_dataService = dataService;
_createValidator = createValidator;
}
public async Task<ContactDTO> CreateContactAsync(CreateContactDTO dto, string userId)
{
// STEP 1: Validate using FluentValidation
var validationResult = await _createValidator.ValidateAsync(dto);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
// STEP 2: Business logic (if any)
// Example: Check for duplicates, apply business rules, etc.
// STEP 3: Map Service DTO → Data DTO
var dataDto = new CreateContactDataDTO
{
FirstName = dto.FirstName,
LastName = dto.LastName,
Email = dto.Email,
Phone = dto.Phone
};
// STEP 4: Call Data Service Layer
var createdContact = await _dataService.CreateAsync(dataDto, userId);
// STEP 5: Map Data DTO → Service DTO and return
return new ContactDTO
{
Id = createdContact.Id,
FirstName = createdContact.FirstName,
LastName = createdContact.LastName,
Email = createdContact.Email,
Phone = createdContact.Phone
};
}
}
```
**Responsibilities:**
- ✅ Validate input using FluentValidation
- ✅ Apply business rules
- ✅ Orchestrate operations
- ✅ Transform data between layers
- ❌ NO HTTP concerns
- ❌ NO direct database access
---
### 3️⃣ **Data Service Layer** (`ContactDataService.cs`)
```csharp
public class ContactDataService : IContactDataService
{
private readonly ApplicationDbContext _db;
public ContactDataService(ApplicationDbContext db)
{
_db = db;
}
public async Task<ContactDataDTO> CreateAsync(CreateContactDataDTO dto, string userId)
{
// STEP 1: Create entity from DTO
var contact = new Contact
{
FirstName = dto.FirstName,
LastName = dto.LastName,
Email = dto.Email,
Phone = dto.Phone,
CreatedBy = userId,
CreatedDate = DateTime.UtcNow,
ModifiedBy = userId,
ModifiedDate = DateTime.UtcNow
};
// STEP 2: Add to database
_db.Contacts.Add(contact);
await _db.SaveChangesAsync();
// STEP 3: Map entity → Data DTO and return
return new ContactDataDTO
{
Id = contact.Id,
FirstName = contact.FirstName,
LastName = contact.LastName,
Email = contact.Email,
Phone = contact.Phone
};
}
}
```
**Responsibilities:**
- ✅ Direct database operations (CRUD)
- ✅ Entity mapping
- ✅ Set audit fields (CreatedBy, CreatedDate, etc.)
- ✅ Transaction management
- ❌ NO validation
- ❌ NO business logic
---
## 🔌 Dependency Injection (DI)
### Automatic Assembly Scanning
Services are **automatically registered** by scanning assemblies using naming convention.
**In `Program.cs`:**
```csharp
builder.Services.AddDataServices(); // Auto-register all Data Services
builder.Services.AddBusinessServices(); // Auto-register all Business Services + Validators
```
### How It Works
**Module Files:**
- `SeaHaven.DataServices/DependencyInjection/DataServicesModule.cs`
- `SeaHaven.Services/DependencyInjection/ServicesModule.cs`
**Process:**
1. Scans assembly for all classes
2. Finds interface matching pattern `I{ClassName}`
3. Registers as Scoped service
4. Also auto-registers FluentValidation validators
### Naming Convention Required
| Interface | Implementation | Registered? |
|-----------|---------------|-------------|
| `IContactService` | `ContactService` | ✅ YES |
| `IContactDataService` | `ContactDataService` | ✅ YES |
| `ISomething` | `SomethingDifferent` | ❌ NO |
**Rule**: Interface must be `I` + Class name exactly
### Usage in Code
Once registered, inject via constructor:
```csharp
public class ContactController : Controller
{
private readonly IContactService _contactService;
public ContactController(IContactService contactService)
{
_contactService = contactService;
}
}
```
### Benefits
✅ No manual registration - just follow naming convention
✅ Auto-discovery - new services work automatically
✅ FluentValidation auto-registered
---
## ✅ FluentValidation
### Validator Example (`CreateContactDTOValidator.cs`)
```csharp
public class CreateContactDTOValidator : AbstractValidator<CreateContactDTO>
{
public CreateContactDTOValidator()
{
RuleFor(x => x.FirstName)
.NotEmpty().WithMessage("First name is required")
.MaximumLength(50).WithMessage("First name cannot exceed 50 characters");
RuleFor(x => x.LastName)
.NotEmpty().WithMessage("Last name is required")
.MaximumLength(50).WithMessage("Last name cannot exceed 50 characters");
RuleFor(x => x.Email)
.NotEmpty().WithMessage("Email is required")
.EmailAddress().WithMessage("Invalid email format")
.MaximumLength(100).WithMessage("Email cannot exceed 100 characters");
RuleFor(x => x.Phone)
.Matches(@"^\+?[1-9]\d{1,14}$").When(x => !string.IsNullOrEmpty(x.Phone))
.WithMessage("Invalid phone number format");
}
}
```
### Where Validation Happens
```
❌ Controller → NO detailed validation (only catches ValidationException)
✅ Service → YES - FluentValidation runs here
❌ DataService → NO validation
```
---
## 🎯 Interface Examples
### Service Interface (`IContactService.cs`)
```csharp
public interface IContactService
{
Task<ContactDTO> CreateContactAsync(CreateContactDTO dto, string userId);
Task<ContactDTO> UpdateContactAsync(int id, UpdateContactDTO dto, string userId);
Task DeleteContactAsync(int id, string userId);
Task<ContactDTO> GetContactByIdAsync(int id);
Task<PagedResult<ContactDTO>> GetContactsPagedAsync(int page, int pageSize, string? search);
}
```
### Data Service Interface (`IContactDataService.cs`)
```csharp
public interface IContactDataService
{
Task<ContactDataDTO> CreateAsync(CreateContactDataDTO dto, string userId);
Task<ContactDataDTO> UpdateAsync(int id, UpdateContactDataDTO dto, string userId);
Task DeleteAsync(int id, string userId);
Task<ContactDataDTO?> GetByIdAsync(int id);
Task<PagedResult<ContactDataDTO>> GetPagedAsync(int page, int pageSize, string? search);
}
```
---
## 📦 DTO Types
### 1. **API DTOs** (`Api.SeaHavenIndustries/DTOs/`)
- Used for HTTP requests/responses
- Map to frontend JSON structure
- Example: `Contact_DTO`, `EditContact_DTO`
### 2. **Service DTOs** (`SeaHaven.Services/DTOs/`)
- Used between Controller ↔ Service
- Business-focused structure
- Example: `CreateContactDTO`, `UpdateContactDTO`, `ContactDTO`
### 3. **Data DTOs** (`SeaHaven.DataServices/DTOs/`)
- Used between Service ↔ DataService
- Database-focused structure
- Example: `CreateContactDataDTO`, `ContactDataDTO`
### DTO Mapping Flow
```
API DTO → Service DTO → Data DTO → Entity
(HTTP) (Business) (Data) (Database)
```
---
## 🚨 Exception Handling Pattern
### Controller Exception Handling (STANDARD PATTERN)
```csharp
[HttpPost("Create")]
public async Task<IActionResult> Create([FromBody] SomeDTO dto)
{
try
{
// Get user ID
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
// Call service
await _service.CreateAsync(dto, userId);
return Ok(new Response { Status = "Success", Message = "Created successfully" });
}
catch (ValidationException vex) // ⚠️ CATCH THIS FIRST
{
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
return BadRequest(new Response { Status = "Validation Error", Message = errors });
}
catch (KeyNotFoundException) // ⚠️ THEN THIS
{
return NotFound(new Response { Status = "Error", Message = "Not found" });
}
catch (Exception ex) // ⚠️ GENERIC LAST
{
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
```
### HTTP Status Code Mapping
- **200 OK** → Success
- **400 Bad Request** → Validation errors
- **401 Unauthorized** → Not authenticated
- **404 Not Found** → Resource not found
- **500 Internal Server Error** → Unexpected errors
---
## 🔐 Authentication & User ID
### Getting User ID (Two Methods)
**Method 1: Using Claims (Preferred)**
```csharp
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
```
**Method 2: Using UserManager (for Identity)**
```csharp
var userId = _userManager.GetUserId(User);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
```
### Audit Fields
Every entity should track:
- `CreatedBy` (string - User ID GUID)
- `CreatedDate` (DateTime)
- `ModifiedBy` (string - User ID GUID)
- `ModifiedDate` (DateTime)
---
## 📋 Naming Conventions
### Controllers
- Named after entity + "Controller"
- Example: `ContactController`, `EmployeeController`
### Services
- Interface: `I[Entity]Service`
- Implementation: `[Entity]Service`
- Example: `IContactService`, `ContactService`
### Data Services
- Interface: `I[Entity]DataService`
- Implementation: `[Entity]DataService`
- Example: `IContactDataService`, `ContactDataService`
### DTOs
- API Layer: `[Entity]_DTO`, `Edit[Entity]_DTO`
- Service Layer: `Create[Entity]DTO`, `Update[Entity]DTO`, `[Entity]DTO`
- Data Layer: `Create[Entity]DataDTO`, `[Entity]DataDTO`
---
## ✅ Best Practices Checklist
### Controllers
- [ ] Use dependency injection for services
- [ ] Get user ID from `User.FindFirstValue(ClaimTypes.NameIdentifier)`
- [ ] Return 401 if userId is null
- [ ] Catch `ValidationException` BEFORE generic `Exception`
- [ ] Return appropriate HTTP status codes
- [ ] NO business logic in controllers
- [ ] NO direct database access (`_db`)
### Services
- [ ] Validate using FluentValidation
- [ ] Throw `ValidationException` for validation errors
- [ ] Throw `KeyNotFoundException` for not found
- [ ] Pass userId to DataService
- [ ] Transform DTOs between layers
- [ ] Implement business rules here
### Data Services
- [ ] Direct database access only
- [ ] Set audit fields (CreatedBy, CreatedDate, ModifiedBy, ModifiedDate)
- [ ] Use async/await for all database operations
- [ ] NO validation logic
- [ ] NO business logic
---
## 🎓 Quick Reference
| Layer | Purpose | Can Access | Cannot Access |
|-------|---------|-----------|---------------|
| **Controller** | HTTP handling, auth | Service layer | Database directly |
| **Service** | Business logic, validation | DataService layer | Database directly |
| **DataService** | Database operations | DbContext, Entities | HTTP context |
| **Data** | Entity definitions | Nothing (POCOs) | Application logic |
---
## 📚 Example: Full CRUD Implementation
See these files for complete examples:
- **Contact**: `ContactController.cs`, `ContactService.cs`, `ContactDataService.cs`
- **Employee**: `EmployeeController.cs`, `EmployeeService.cs`, `EmployeeDataService.cs`
- **Asset**: `AssetController.cs`, `AssetService.cs`, `AssetDataService.cs`
All follow the same Clean Architecture pattern!
---
## 🔐 Configuration & Secrets
No secrets are stored in source. The committed `appsettings*.json` files hold only
`${PLACEHOLDER}` tokens; real values must be supplied at runtime through one of the
standard .NET configuration sources (later sources win):
1. `appsettings.json` / `appsettings.{Environment}.json` — committed, placeholders only.
2. **User Secrets** (local dev): `dotnet user-secrets set "Section:Key" "value"`.
3. **Environment variables** — use `__` (double underscore) as the section separator.
In AWS Elastic Beanstalk these are the environment properties.
See `.env.example` in the repo root for the full list. Required values:
| Setting | Env-var form | Used by | Notes |
|---|---|---|---|
| `ConnectionStrings:DefaultConnection` | `ConnectionStrings__DefaultConnection` | Both projects | SQL Server connection string |
| `SendGrid:ApiKey` | `SendGrid__ApiKey` | Both projects | Transactional email |
| `JWT:Secret` | `JWT__Secret` | `Api.SeaHavenIndustries` | JWT signing key |
| `GoogleMaps:ApiKey` | `GoogleMaps__ApiKey` | `SeaHavenIndustries` (Blazor) | Maps/Places; read in `App.razor` + `Home.razor` |
### AWS credentials
`UploadFileHp` (S3 uploads) uses the **AWS SDK default credential chain** — it no longer
hardcodes access keys. Credentials resolve from `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`
env vars, the shared profile/SSO, or (preferred in production) the EC2/ECS instance role.
The region comes from `AddDefaultAWSOptions` in `Program.cs`.
> ⚠️ The secrets previously committed to this repo (DB password, SendGrid key, JWT secret,
> Google Maps keys, AWS access key) should be considered compromised and **rotated**.

View file

@ -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; }

View file

@ -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>

View file

@ -0,0 +1,92 @@
namespace Data.SeaHavenIndustries.Enums
{
public enum WorkOrderStatus
{
Open = 1,
InProgress = 2,
Completed = 3,
Cancelled = 4,
OnHold = 5
}
public enum WorkOrderPriority
{
Low = 1,
Medium = 2,
High = 3,
Critical = 4
}
public enum AuditAction
{
Create = 1,
Edit = 2,
Delete = 3,
StatusChange = 4,
AssignmentChange = 5,
Dispatch = 6,
DispatchUpdate = 7,
DispatchVerify = 8
}
public enum ContactType
{
Primary = 1,
Secondary = 2,
Billing = 3,
Technical = 4
}
public enum AssetStatus
{
Active = 1,
Inactive = 2,
UnderMaintenance = 3,
Retired = 4
}
public enum DispatchStatus
{
Pending = 1,
Assigned = 2,
InProgress = 3,
Completed = 4,
Cancelled = 5
}
public enum FollowUpStatus
{
Open = 1,
InProgress = 2,
Resolved = 3,
Closed = 4
}
public enum PMScheduleFrequency
{
Daily = 1,
Weekly = 2,
Monthly = 3,
Quarterly = 4,
SemiAnnual = 5,
Annual = 6
}
public enum CommentType
{
General = 1,
Internal = 2,
Customer = 3,
Vendor = 4,
System = 5
}
public enum RecordType
{
WorkOrder = 1,
Dispatch = 2,
Quote = 3,
Contact = 4,
Asset = 5
}
}

View file

@ -86,8 +86,9 @@ namespace Data.SeaHavenIndustries
public Employee Employee { get; set; } = null!;
}
public class EmployeeEmail : FullAuditEntity
public class EmployeeEmail
{
public int Id { get; set; }
public int EmployeeId { get; set; }
public Employee? Employee { get; set; }
@ -99,22 +100,28 @@ namespace Data.SeaHavenIndustries
public bool? IsPrimary { get; set; }
}
public class Department : FullAuditEntity
public class Department
{
public int Id { get; set; }
[MaxLength(200)]
public string Name { get; set; } = null!;
public ICollection<Employee>? Employees { get; set; }
}
public class JobTitle : FullAuditEntity
public class JobTitle
{
public int Id { get; set; }
[MaxLength(200)]
public string Name { get; set; } = null!;
public ICollection<Employee>? Employees { get; set; }
}
public class Region : FullAuditEntity
public class Region
{
public int Id { get; set; }
[MaxLength(200)]
public string Name { get; set; } = null!;
public ICollection<Employee>? Employees { get; set; }

View 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;
}
}

View file

@ -6,21 +6,25 @@ namespace Data.SeaHavenIndustries
{
public class Locations : FullAuditEntity
{
public int Id { get; set; }
public string? Title { get; set; }
public string? Name { get; set; }
public string? Address { get; set; }
public string? City { get; set; }
public string? State { get; set; }
public string? ZipCode { get; set; }
public string? Phone { get; set; }
public string? Contact { get; set; }
public string? ContactEmail { get; set; }
public string? Status { get; set; }
public string? Latitude { get; set; }
public string? Longitude { get; set; }
public int? AccountId { get; set; }
[ForeignKey("AccountId")]
public Accounts? Account { get; set; }
// Address fields - match actual database columns
public string? Address1 { get; set; }
public string? Address2 { get; set; }
public string? City { get; set; }
public string? State { get; set; }
public string? Zip { get; set; }
// Contact fields - match actual database columns
public string? PhoneNumber { get; set; }
public string? Email { get; set; }
public string? Status { get; set; }
// Navigation Properties
public ICollection<Template>? Templates { get; set; }
public ICollection<WorkOrder>? workOrders { get; set; }
}

View file

@ -0,0 +1,35 @@
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
namespace SeaHaven.DataServices.DependencyInjection
{
public static class DataServicesModule
{
private static readonly List<Type> ExcludedTypes = new();
public static IServiceCollection AddDataServices(this IServiceCollection services)
{
var assembly = Assembly.GetExecutingAssembly();
var allClasses = assembly.GetTypes()
.Where(t => t.IsClass &&
!t.IsAbstract &&
!t.IsGenericType &&
!ExcludedTypes.Contains(t))
.ToList();
foreach (var implementationType in allClasses)
{
var defaultInterface = implementationType.GetInterfaces()
.FirstOrDefault(i => i.Name == $"I{implementationType.Name}");
if (defaultInterface != null)
{
services.AddScoped(defaultInterface, implementationType);
}
}
return services;
}
}
}

View file

@ -0,0 +1,100 @@
using Data.SeaHavenIndustries;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Interfaces;
namespace SeaHaven.DataServices.Implementation
{
public class AccountDataService : IAccountDataService
{
private readonly ApplicationDbContext _context;
public AccountDataService(ApplicationDbContext context)
{
_context = context;
}
public async Task<Accounts?> GetByIdAsync(int id)
{
return await _context.Accounts.FindAsync(id);
}
public async Task<Accounts?> GetByIdWithDetailsAsync(int id)
{
return await _context.Accounts
.Include(a => a.Contacts)
.Include(a => a.Addresses)
.FirstOrDefaultAsync(a => a.Id == id);
}
public async Task<IEnumerable<Accounts>> GetAllAsync()
{
return await _context.Accounts.ToListAsync();
}
public async Task<(IEnumerable<Accounts> Items, int TotalCount)> GetPagedAsync(
int page,
int pageSize,
string? search = null)
{
search ??= "";
var query = _context.Accounts
.Where(a => a.IsDeleted == false || a.IsDeleted == null);
if (!string.IsNullOrWhiteSpace(search))
{
var s = search.ToLower();
query = query.Where(a =>
(a.Name ?? "").ToLower().Contains(s) ||
(a.Email ?? "").ToLower().Contains(s) ||
(a.Phone ?? "").Contains(s) ||
(a.Website ?? "").ToLower().Contains(s)
);
}
var totalCount = await query.CountAsync();
var items = await query
.OrderByDescending(a => a.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return (items, totalCount);
}
public async Task<Accounts> AddAsync(Accounts account)
{
account.CreatedDate = DateTime.Now;
await _context.Accounts.AddAsync(account);
await _context.SaveChangesAsync();
return account;
}
public async Task UpdateAsync(Accounts account)
{
account.LastModificationTime = DateTime.Now;
_context.Accounts.Update(account);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var entity = await GetByIdAsync(id);
if (entity != null)
{
_context.Accounts.Remove(entity);
await _context.SaveChangesAsync();
}
}
public async Task<bool> ExistsAsync(int id)
{
return await _context.Accounts.AnyAsync(a => a.Id == id);
}
public async Task<int> CountAsync()
{
return await _context.Accounts.CountAsync();
}
}
}

View file

@ -0,0 +1,108 @@
using Data.SeaHavenIndustries;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Interfaces;
namespace SeaHaven.DataServices.Implementation
{
public class AssetDataService : IAssetDataService
{
private readonly ApplicationDbContext _context;
public AssetDataService(ApplicationDbContext context)
{
_context = context;
}
public async Task<Assets?> GetByIdAsync(int id)
{
return await _context.Assets.FindAsync(id);
}
public async Task<Assets?> GetByIdWithDetailsAsync(int id)
{
return await _context.Assets
.Include(a => a.Account)
.Include(a => a.Location)
.FirstOrDefaultAsync(a => a.Id == id);
}
public async Task<IEnumerable<Assets>> GetAllAsync()
{
return await _context.Assets.ToListAsync();
}
public async Task<IEnumerable<Assets>> GetByLocationIdAsync(int locationId)
{
return await _context.Assets
.Where(a => a.LocationId == locationId)
.ToListAsync();
}
public async Task<(IEnumerable<Assets> Items, int TotalCount)> GetPagedAsync(
int page,
int pageSize,
string? search = null)
{
search ??= "";
var query = _context.Assets
.Include(a => a.Account)
.Include(a => a.Location)
.AsQueryable();
if (!string.IsNullOrWhiteSpace(search))
{
var s = search.ToLower();
query = query.Where(a =>
(a.Name != null && a.Name.ToLower().Contains(s)) ||
(a.AssetCode != null && a.AssetCode.ToLower().Contains(s)) ||
(a.Description != null && a.Description.ToLower().Contains(s))
);
}
var totalCount = await query.CountAsync();
var items = await query
.OrderByDescending(a => a.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return (items, totalCount);
}
public async Task<Assets> AddAsync(Assets asset)
{
asset.CreatedDate = DateTime.Now;
await _context.Assets.AddAsync(asset);
await _context.SaveChangesAsync();
return asset;
}
public async Task UpdateAsync(Assets asset)
{
asset.LastModificationTime = DateTime.Now;
_context.Assets.Update(asset);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var entity = await GetByIdAsync(id);
if (entity != null)
{
_context.Assets.Remove(entity);
await _context.SaveChangesAsync();
}
}
public async Task<bool> ExistsAsync(int id)
{
return await _context.Assets.AnyAsync(a => a.Id == id);
}
public async Task<int> CountAsync()
{
return await _context.Assets.CountAsync();
}
}
}

View file

@ -0,0 +1,56 @@
using Data.SeaHavenIndustries;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Interfaces;
namespace SeaHaven.DataServices.Implementation
{
public class CategoryDataService : ICategoryDataService
{
private readonly ApplicationDbContext _context;
public CategoryDataService(ApplicationDbContext context)
{
_context = context;
}
public async Task<Category?> GetByIdAsync(int id)
{
return await _context.Categories.FindAsync(id);
}
public async Task<IEnumerable<Category>> GetAllAsync()
{
return await _context.Categories.ToListAsync();
}
public async Task<Category> AddAsync(Category category)
{
category.CreatedDate = DateTime.Now;
await _context.Categories.AddAsync(category);
await _context.SaveChangesAsync();
return category;
}
public async Task UpdateAsync(Category category)
{
category.LastModificationTime = DateTime.Now;
_context.Categories.Update(category);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var entity = await GetByIdAsync(id);
if (entity != null)
{
_context.Categories.Remove(entity);
await _context.SaveChangesAsync();
}
}
public async Task<bool> ExistsAsync(int id)
{
return await _context.Categories.AnyAsync(c => c.Id == id);
}
}
}

View file

@ -0,0 +1,75 @@
using Data.SeaHavenIndustries;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Interfaces;
namespace SeaHaven.DataServices.Implementation
{
public class CommentDataService : ICommentDataService
{
private readonly ApplicationDbContext _context;
public CommentDataService(ApplicationDbContext context)
{
_context = context;
}
public async Task<Comments?> GetByIdAsync(int id)
{
return await _context.Comments.FindAsync(id);
}
public async Task<IEnumerable<Comments>> GetAllAsync()
{
return await _context.Comments
.Include(c => c.ApplicationUser)
.ToListAsync();
}
public async Task<IEnumerable<Comments>> GetByWorkOrderIdAsync(int workOrderId)
{
return await _context.Comments
.Where(c => c.WorkerOrderId == workOrderId)
.Include(c => c.ApplicationUser)
.OrderBy(c => c.CreatedDate)
.ToListAsync();
}
public async Task<IEnumerable<Comments>> GetByDispatchIdAsync(int dispatchId)
{
return await _context.Comments
.Where(c => c.DispatchId == dispatchId)
.Include(c => c.ApplicationUser)
.OrderBy(c => c.CreatedDate)
.ToListAsync();
}
public async Task<Comments> AddAsync(Comments comment)
{
comment.CreatedDate = DateTime.UtcNow;
await _context.Comments.AddAsync(comment);
await _context.SaveChangesAsync();
return comment;
}
public async Task UpdateAsync(Comments comment)
{
_context.Comments.Update(comment);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var entity = await GetByIdAsync(id);
if (entity != null)
{
_context.Comments.Remove(entity);
await _context.SaveChangesAsync();
}
}
public async Task<bool> ExistsAsync(int id)
{
return await _context.Comments.AnyAsync(c => c.Id == id);
}
}
}

View file

@ -0,0 +1,160 @@
using Data.SeaHavenIndustries;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Interfaces;
namespace SeaHaven.DataServices.Implementation
{
public class ContactDataService : IContactDataService
{
private readonly ApplicationDbContext _context;
public ContactDataService(ApplicationDbContext context)
{
_context = context;
}
public async Task<Contacts?> GetByIdAsync(int id)
{
return await _context.Contacts.FindAsync(id);
}
public async Task<Contacts?> GetByIdWithDetailsAsync(int id)
{
return await _context.Contacts
.Include(c => c.Location)
.Include(c => c.Account)
.FirstOrDefaultAsync(c => c.Id == id);
}
public async Task<IEnumerable<Contacts>> GetAllAsync()
{
return await _context.Contacts.ToListAsync();
}
public async Task<IEnumerable<Contacts>> GetByLocationIdAsync(int locationId)
{
return await _context.Contacts
.Where(c => c.LocationId == locationId)
.ToListAsync();
}
public async Task<IEnumerable<Contacts>> GetByAccountIdAsync(int accountId)
{
return await _context.Contacts
.Where(c => c.AccountId == accountId)
.ToListAsync();
}
public async Task<IEnumerable<Contacts>> GetByTypeAsync(string type)
{
return await _context.Contacts
.Where(c => c.ContactType == type)
.ToListAsync();
}
public async Task<Contacts?> GetByEmailAsync(string email)
{
return await _context.Contacts
.FirstOrDefaultAsync(c => c.Email == email);
}
public async Task<Contacts?> GetByPhoneAsync(string phone)
{
return await _context.Contacts
.FirstOrDefaultAsync(c => c.PhoneNumber == phone);
}
public async Task<(IEnumerable<Contacts> Items, int TotalCount)> GetPagedAsync(
int page,
int pageSize,
string? search = null,
string? type = null)
{
search ??= "";
var query = _context.Contacts.AsQueryable();
if (!string.IsNullOrWhiteSpace(search))
{
var s = search.ToLower();
query = query.Where(c =>
(c.FirstName != null && c.FirstName.ToLower().Contains(s)) ||
(c.LastName != null && c.LastName.ToLower().Contains(s)) ||
(c.Email != null && c.Email.ToLower().Contains(s)) ||
(c.PhoneNumber != null && c.PhoneNumber.Contains(s)));
}
if (!string.IsNullOrWhiteSpace(type))
{
query = query.Where(c => c.ContactType == type);
}
var totalCount = await query.CountAsync();
var items = await query
.OrderByDescending(c => c.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return (items, totalCount);
}
public async Task<Contacts> AddAsync(Contacts contact)
{
contact.CreatedDate = DateTime.Now;
await _context.Contacts.AddAsync(contact);
await _context.SaveChangesAsync();
return contact;
}
public async Task UpdateAsync(Contacts contact)
{
contact.LastModificationTime = DateTime.Now;
_context.Contacts.Update(contact);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var entity = await GetByIdAsync(id);
if (entity != null)
{
_context.Contacts.Remove(entity);
await _context.SaveChangesAsync();
}
}
public async Task<bool> ExistsAsync(int id)
{
return await _context.Contacts.AnyAsync(c => c.Id == id);
}
public async Task<bool> EmailExistsAsync(string email, int? excludeId = null)
{
return await _context.Contacts.AnyAsync(c =>
c.Email == email &&
(excludeId == null || c.Id != excludeId));
}
public async Task<bool> PhoneExistsAsync(string phone, int? excludeId = null)
{
return await _context.Contacts.AnyAsync(c =>
c.PhoneNumber == phone &&
(excludeId == null || c.Id != excludeId));
}
public async Task<int> CountAsync()
{
return await _context.Contacts.CountAsync();
}
public async Task<int> CountByAccountIdAsync(int accountId)
{
return await _context.Contacts.CountAsync(c => c.AccountId == accountId);
}
public async Task<int> CountByTypeAsync(string type)
{
return await _context.Contacts.CountAsync(c => c.ContactType == type);
}
}
}

View file

@ -0,0 +1,220 @@
using Data.SeaHavenIndustries;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Interfaces;
namespace SeaHaven.DataServices.Implementation
{
public class DispatchDataService : IDispatchDataService
{
private readonly ApplicationDbContext _context;
public DispatchDataService(ApplicationDbContext context)
{
_context = context;
}
public async Task<Dispatch?> GetByIdAsync(int id)
{
return await _context.Dispatches.FindAsync(id);
}
public async Task<Dispatch?> GetByIdWithDetailsAsync(int id)
{
return await _context.Dispatches
.Include(d => d.Vendor)
.FirstOrDefaultAsync(d => d.Id == id);
}
public async Task<IEnumerable<Dispatch>> GetAllAsync()
{
return await _context.Dispatches.ToListAsync();
}
public async Task<IEnumerable<Dispatch>> GetByVendorIdAsync(int vendorId)
{
return await _context.Dispatches
.Where(d => d.VendorId == vendorId)
.ToListAsync();
}
public async Task<IEnumerable<Dispatch>> GetByStatusAsync(string status)
{
return await _context.Dispatches
.Where(d => d.Status == status)
.ToListAsync();
}
public async Task<(IEnumerable<Dispatch> Items, int TotalCount)> GetPagedAsync(
int page,
int pageSize,
string? search = null,
string? status = null)
{
var query = _context.Dispatches.AsQueryable();
if (!string.IsNullOrWhiteSpace(search))
{
query = query.Where(d => d.DispatchNumber!.Contains(search));
}
if (!string.IsNullOrWhiteSpace(status))
{
query = query.Where(d => d.Status == status);
}
var totalCount = await query.CountAsync();
var items = await query
.OrderByDescending(d => d.CreatedDate)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return (items, totalCount);
}
public async Task<Dispatch> AddAsync(Dispatch dispatch)
{
dispatch.CreatedDate = DateTime.Now;
await _context.Dispatches.AddAsync(dispatch);
await _context.SaveChangesAsync();
return dispatch;
}
public async Task UpdateAsync(Dispatch dispatch)
{
dispatch.LastModificationTime = DateTime.Now;
_context.Dispatches.Update(dispatch);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var entity = await GetByIdAsync(id);
if (entity != null)
{
_context.Dispatches.Remove(entity);
await _context.SaveChangesAsync();
}
}
public async Task<bool> ExistsAsync(int id)
{
return await _context.Dispatches.AnyAsync(d => d.Id == id);
}
public async Task<string> GenerateNextDispatchNumberAsync()
{
return await Task.FromResult($"DISP-{DateTime.Now:yyyyMMdd}-{new Random().Next(1000, 9999)}");
}
public async Task<int> CountAsync()
{
return await _context.Dispatches.CountAsync();
}
public async Task<IEnumerable<Dispatch>> GetByWorkOrderIdAsync(int workOrderId)
{
var dispatchIds = await _context.DispatchWorkOrders
.Where(dwo => dwo.WorkOrderId == workOrderId)
.Select(dwo => dwo.DispatchId)
.Distinct()
.ToListAsync();
var directIds = await _context.Dispatches
.Where(d => d.WorkOrderId == workOrderId)
.Select(d => d.Id)
.ToListAsync();
var allIds = dispatchIds.Union(directIds).Distinct().ToList();
return await _context.Dispatches
.Where(d => allIds.Contains(d.Id))
.Include(d => d.Vendor)
.ToListAsync();
}
public async Task<IEnumerable<Dispatch>> GetByWorkOrderIdsAsync(IEnumerable<int> workOrderIds)
{
return await _context.Dispatches
.Where(d => workOrderIds.Contains(d.WorkOrderId ?? 0))
.Include(d => d.Vendor)
.ToListAsync();
}
public async Task<string> GenerateNextPONumberAsync()
{
var lastPO = await _context.Dispatches
.Where(d => d.PONumber != null)
.OrderByDescending(d => d.PONumber)
.Select(d => d.PONumber)
.FirstOrDefaultAsync();
int nextPO = 1000;
if (!string.IsNullOrEmpty(lastPO) && int.TryParse(lastPO, out int lastNum))
{
nextPO = lastNum + 1;
}
return nextPO.ToString();
}
public async Task<IEnumerable<DispatchChecklistItem>> GetChecklistItemsAsync(int dispatchId)
{
return await _context.DispatchChecklistItems
.Where(ci => ci.DispatchId == dispatchId)
.OrderBy(ci => ci.SortOrder)
.ToListAsync();
}
public async Task<DispatchChecklistItem?> GetChecklistItemByIdAsync(int id)
{
return await _context.DispatchChecklistItems.FindAsync(id);
}
public async Task AddChecklistItemAsync(DispatchChecklistItem item)
{
await _context.DispatchChecklistItems.AddAsync(item);
await _context.SaveChangesAsync();
}
public async Task UpdateChecklistItemAsync(DispatchChecklistItem item)
{
_context.DispatchChecklistItems.Update(item);
await _context.SaveChangesAsync();
}
public async Task<IEnumerable<DispatchSignoff>> GetSignoffsAsync(int dispatchId)
{
return await _context.DispatchSignoffs
.Where(s => s.DispatchId == dispatchId)
.ToListAsync();
}
public async Task AddSignoffAsync(DispatchSignoff signoff)
{
signoff.SignedAt = DateTime.UtcNow;
await _context.DispatchSignoffs.AddAsync(signoff);
await _context.SaveChangesAsync();
}
public async Task<bool> HasSignoffTypeAsync(int dispatchId, string signoffType)
{
return await _context.DispatchSignoffs
.AnyAsync(s => s.DispatchId == dispatchId && s.SignoffType == signoffType);
}
public async Task<IEnumerable<DispatchWorkOrder>> GetDispatchWorkOrdersAsync(int dispatchId)
{
return await _context.DispatchWorkOrders
.Where(dwo => dwo.DispatchId == dispatchId)
.Include(dwo => dwo.WorkOrder)
.ToListAsync();
}
public async Task AddDispatchWorkOrderAsync(DispatchWorkOrder dispatchWorkOrder)
{
await _context.DispatchWorkOrders.AddAsync(dispatchWorkOrder);
await _context.SaveChangesAsync();
}
}
}

View file

@ -0,0 +1,108 @@
using Data.SeaHavenIndustries;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Interfaces;
namespace SeaHaven.DataServices.Implementation
{
public class EmployeeDataService : IEmployeeDataService
{
private readonly ApplicationDbContext _context;
public EmployeeDataService(ApplicationDbContext context)
{
_context = context;
}
public async Task<Employee?> GetByIdAsync(int id)
{
return await _context.Employees.FindAsync(id);
}
public async Task<Employee?> GetByIdWithDetailsAsync(int id)
{
return await _context.Employees
.Include(e => e.Department)
.Include(e => e.JobTitle)
.Include(e => e.Region)
.Include(e => e.Emails)
.Include(e => e.Phones)
.FirstOrDefaultAsync(e => e.Id == id);
}
public async Task<IEnumerable<Employee>> GetAllAsync()
{
return await _context.Employees.ToListAsync();
}
public async Task<(IEnumerable<Employee> Items, int TotalCount)> GetPagedAsync(
int page,
int pageSize,
string? search = null)
{
search ??= "";
var query = _context.Employees
.Include(e => e.JobTitle)
.Include(e => e.Department)
.Include(e => e.Region)
.Include(e => e.Emails)
.Include(e => e.Phones)
.Where(e => !e.IsDeleted.HasValue || e.IsDeleted == false);
if (!string.IsNullOrWhiteSpace(search))
{
var s = search.ToLower();
query = query.Where(e =>
(e.FirstName != null && e.FirstName.ToLower().Contains(s)) ||
(e.LastName != null && e.LastName.ToLower().Contains(s)) ||
e.Emails.Any(x => x.Address != null && x.Address.ToLower().Contains(s)) ||
e.Phones.Any(x => x.Number != null && x.Number.Contains(search))
);
}
var totalCount = await query.CountAsync();
var items = await query
.OrderByDescending(e => e.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return (items, totalCount);
}
public async Task<Employee> AddAsync(Employee employee)
{
employee.CreatedDate = DateTime.Now;
await _context.Employees.AddAsync(employee);
await _context.SaveChangesAsync();
return employee;
}
public async Task UpdateAsync(Employee employee)
{
employee.LastModificationTime = DateTime.Now;
_context.Employees.Update(employee);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var entity = await GetByIdAsync(id);
if (entity != null)
{
_context.Employees.Remove(entity);
await _context.SaveChangesAsync();
}
}
public async Task<bool> ExistsAsync(int id)
{
return await _context.Employees.AnyAsync(e => e.Id == id);
}
public async Task<int> CountAsync()
{
return await _context.Employees.CountAsync();
}
}
}

View file

@ -0,0 +1,124 @@
using Data.SeaHavenIndustries;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Interfaces;
namespace SeaHaven.DataServices.Implementation
{
public class FollowUpDataService : IFollowUpDataService
{
private readonly ApplicationDbContext _context;
public FollowUpDataService(ApplicationDbContext context)
{
_context = context;
}
public async Task<FollowUps?> GetByIdAsync(int id)
{
return await _context.FollowUps.FindAsync(id);
}
public async Task<FollowUps?> GetByIdWithDetailsAsync(int id)
{
return await _context.FollowUps
.Include(x => x.Employee)
.Include(x => x.Account)
.Include(x => x.Location)
// Removed .Include(x => x.WorkOrder) due to schema mismatch - WorkOrder has columns not in DB
.FirstOrDefaultAsync(f => f.Id == id);
}
public async Task<IEnumerable<FollowUps>> GetAllAsync()
{
return await _context.FollowUps.ToListAsync();
}
public async Task<IEnumerable<FollowUps>> GetByWorkOrderIdAsync(int workOrderId)
{
return await _context.FollowUps
.Where(f => f.WorkOrderId == workOrderId)
.ToListAsync();
}
public async Task<IEnumerable<FollowUps>> GetByUserIdAsync(string userId)
{
return await _context.FollowUps
.Where(f => f.createdby == userId)
.ToListAsync();
}
public async Task<(IEnumerable<FollowUps> Items, int TotalCount)> GetPagedAsync(
int page,
int pageSize,
string? search = null,
string? status = null)
{
search ??= "";
status = string.IsNullOrWhiteSpace(status) ? "Open" : status;
var query = _context.FollowUps
.Include(x => x.Employee)
.Include(x => x.Account)
.Include(x => x.Location)
// Removed .Include(x => x.WorkOrder) due to schema mismatch - WorkOrder has columns not in DB
.Where(x => x.Status == status);
if (!string.IsNullOrWhiteSpace(search))
{
var s = search.ToLower();
query = query.Where(x =>
(x.Reason ?? "").ToLower().Contains(s) ||
(x.Account != null && (x.Account.Name ?? "").ToLower().Contains(s)) ||
(x.Employee != null &&
(((x.Employee.FirstName ?? "") + " " + (x.Employee.LastName ?? ""))
.ToLower().Contains(s))
)
);
}
var totalCount = await query.CountAsync();
var items = await query
.OrderByDescending(f => f.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return (items, totalCount);
}
public async Task<FollowUps> AddAsync(FollowUps followUp)
{
followUp.CreatedDate = DateTime.Now;
await _context.FollowUps.AddAsync(followUp);
await _context.SaveChangesAsync();
return followUp;
}
public async Task UpdateAsync(FollowUps followUp)
{
followUp.LastModificationTime = DateTime.Now;
_context.FollowUps.Update(followUp);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var entity = await GetByIdAsync(id);
if (entity != null)
{
_context.FollowUps.Remove(entity);
await _context.SaveChangesAsync();
}
}
public async Task<bool> ExistsAsync(int id)
{
return await _context.FollowUps.AnyAsync(f => f.Id == id);
}
public async Task<int> CountAsync()
{
return await _context.FollowUps.CountAsync();
}
}
}

View file

@ -0,0 +1,132 @@
using Data.SeaHavenIndustries;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Interfaces;
namespace SeaHaven.DataServices.Implementation
{
public class LocationDataService : ILocationDataService
{
private readonly ApplicationDbContext _context;
public LocationDataService(ApplicationDbContext context)
{
_context = context;
}
public async Task<Locations?> GetByIdAsync(int id)
{
return await _context.Locations.FindAsync(id);
}
public async Task<Locations?> GetByIdWithDetailsAsync(int id)
{
return await _context.Locations
.FirstOrDefaultAsync(l => l.Id == id);
}
public async Task<IEnumerable<Locations>> GetAllAsync()
{
return await _context.Locations.ToListAsync();
}
public async Task<IEnumerable<Locations>> GetByAccountIdAsync(int accountId)
{
// AccountId doesn't exist in database - return all for now
// TODO: Add AccountId column to database if needed
return await _context.Locations.ToListAsync();
}
public async Task<(IEnumerable<Locations> Items, int TotalCount)> GetPagedAsync(
int page,
int pageSize,
string? search = null)
{
var query = _context.Locations.AsQueryable();
if (!string.IsNullOrWhiteSpace(search))
{
query = query.Where(l =>
l.Name!.Contains(search) ||
l.City!.Contains(search));
}
var totalCount = await query.CountAsync();
var items = await query
.OrderBy(l => l.Name)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return (items, totalCount);
}
public async Task<Locations> AddAsync(Locations location)
{
location.CreatedDate = DateTime.Now;
await _context.Locations.AddAsync(location);
await _context.SaveChangesAsync();
return location;
}
public async Task UpdateAsync(Locations location)
{
location.LastModificationTime = DateTime.Now;
_context.Locations.Update(location);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var entity = await GetByIdAsync(id);
if (entity != null)
{
_context.Locations.Remove(entity);
await _context.SaveChangesAsync();
}
}
public async Task<bool> ExistsAsync(int id)
{
return await _context.Locations.AnyAsync(l => l.Id == id);
}
public async Task<int> CountAsync()
{
return await _context.Locations.CountAsync();
}
public async Task<(IEnumerable<object> Items, int TotalCount)> GetAddressbookPagedAsync(
int page,
int pageSize,
string? search = null)
{
search ??= "";
var query = _context.Locations.AsQueryable();
if (!string.IsNullOrWhiteSpace(search))
{
var s = search.ToLower();
query = query.Where(w =>
(w.Name ?? "").ToLower().Contains(s) ||
(w.Title ?? "").ToLower().Contains(s));
}
var totalCount = await query.CountAsync();
var data = await query
.Select(n => new
{
n.Id,
n.Name,
n.Title
})
.OrderByDescending(d => d.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return (data, totalCount);
}
}
}

View file

@ -0,0 +1,100 @@
using Data.SeaHavenIndustries;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Interfaces;
namespace SeaHaven.DataServices.Implementation
{
public class PMScheduleDataService : IPMScheduleDataService
{
private readonly ApplicationDbContext _context;
public PMScheduleDataService(ApplicationDbContext context)
{
_context = context;
}
public async Task<PMSchedules?> GetByIdAsync(int id)
{
return await _context.PMSchedules.FindAsync(id);
}
public async Task<PMSchedules?> GetByIdWithDetailsAsync(int id)
{
return await _context.PMSchedules
.FirstOrDefaultAsync(p => p.Id == id);
}
public async Task<IEnumerable<PMSchedules>> GetAllAsync()
{
return await _context.PMSchedules.ToListAsync();
}
public async Task<IEnumerable<PMSchedules>> GetAllWithDetailsAsync()
{
return await _context.PMSchedules
.Include(x => x.Asset)
.Include(x => x.Location)
.OrderByDescending(x => x.Id)
.ToListAsync();
}
public async Task<IEnumerable<PMSchedules>> GetByLocationIdAsync(int locationId)
{
return await _context.PMSchedules
.Where(p => p.LocationId == locationId)
.ToListAsync();
}
public async Task<(IEnumerable<PMSchedules> Items, int TotalCount)> GetPagedAsync(
int page,
int pageSize,
string? search = null)
{
var query = _context.PMSchedules.AsQueryable();
var totalCount = await query.CountAsync();
var items = await query
.OrderBy(p => p.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return (items, totalCount);
}
public async Task<PMSchedules> AddAsync(PMSchedules schedule)
{
schedule.CreatedDate = DateTime.Now;
await _context.PMSchedules.AddAsync(schedule);
await _context.SaveChangesAsync();
return schedule;
}
public async Task UpdateAsync(PMSchedules schedule)
{
schedule.LastModificationTime = DateTime.Now;
_context.PMSchedules.Update(schedule);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var entity = await GetByIdAsync(id);
if (entity != null)
{
_context.PMSchedules.Remove(entity);
await _context.SaveChangesAsync();
}
}
public async Task<bool> ExistsAsync(int id)
{
return await _context.PMSchedules.AnyAsync(p => p.Id == id);
}
public async Task<int> CountAsync()
{
return await _context.PMSchedules.CountAsync();
}
}
}

View file

@ -0,0 +1,75 @@
using Data.SeaHavenIndustries;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Interfaces;
namespace SeaHaven.DataServices.Implementation
{
public class QuotesDataService : IQuotesDataService
{
private readonly ApplicationDbContext _context;
public QuotesDataService(ApplicationDbContext context)
{
_context = context;
}
public async Task<Quotes?> GetByIdAsync(int id)
{
return await _context.Quotes.FindAsync(id);
}
public async Task<IEnumerable<Quotes>> GetAllAsync()
{
return await _context.Quotes.ToListAsync();
}
public async Task<IEnumerable<Quotes>> GetByWorkOrderIdAsync(int workOrderId)
{
return await _context.Quotes
.Where(q => q.WorkorderId == workOrderId)
.ToListAsync();
}
public async Task<Quotes> AddAsync(Quotes quote)
{
await _context.Quotes.AddAsync(quote);
await _context.SaveChangesAsync();
return quote;
}
public async Task UpdateAsync(Quotes quote)
{
_context.Quotes.Update(quote);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var entity = await GetByIdAsync(id);
if (entity != null)
{
_context.Quotes.Remove(entity);
await _context.SaveChangesAsync();
}
}
public async Task<bool> ExistsAsync(int id)
{
return await _context.Quotes.AnyAsync(q => q.Id == id);
}
public async Task UnlinkFromWorkOrderAsync(int workOrderId)
{
var quotes = await _context.Quotes
.Where(q => q.WorkorderId == workOrderId)
.ToListAsync();
foreach (var quote in quotes)
{
quote.WorkorderId = null;
}
await _context.SaveChangesAsync();
}
}
}

View file

@ -0,0 +1,61 @@
using Data.SeaHavenIndustries;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Interfaces;
namespace SeaHaven.DataServices.Implementation
{
public class TaskListTemplateDataService : ITaskListTemplateDataService
{
private readonly ApplicationDbContext _context;
public TaskListTemplateDataService(ApplicationDbContext context)
{
_context = context;
}
public async Task<TaskListTemplate?> GetByIdAsync(int id)
{
return await _context.TaskListTemplates.FindAsync(id);
}
public async Task<TaskListTemplate?> GetByIdWithItemsAsync(int id)
{
return await _context.TaskListTemplates
.Include(t => t.Items)
.FirstOrDefaultAsync(t => t.Id == id);
}
public async Task<IEnumerable<TaskListTemplate>> GetAllAsync()
{
return await _context.TaskListTemplates.ToListAsync();
}
public async Task<TaskListTemplate> AddAsync(TaskListTemplate template)
{
await _context.TaskListTemplates.AddAsync(template);
await _context.SaveChangesAsync();
return template;
}
public async Task UpdateAsync(TaskListTemplate template)
{
_context.TaskListTemplates.Update(template);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var entity = await GetByIdAsync(id);
if (entity != null)
{
_context.TaskListTemplates.Remove(entity);
await _context.SaveChangesAsync();
}
}
public async Task<bool> ExistsAsync(int id)
{
return await _context.TaskListTemplates.AnyAsync(t => t.Id == id);
}
}
}

View file

@ -0,0 +1,37 @@
using Data.SeaHavenIndustries;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Interfaces;
namespace SeaHaven.DataServices.Implementation
{
public class UserDataService : IUserDataService
{
private readonly ApplicationDbContext _context;
public UserDataService(ApplicationDbContext context)
{
_context = context;
}
public async Task<ApplicationUser?> GetByIdAsync(string id)
{
return await _context.Users.FindAsync(id);
}
public async Task<IEnumerable<ApplicationUser>> GetAllAsync()
{
return await _context.Users.ToListAsync();
}
public async Task<ApplicationUser?> GetByEmailAsync(string email)
{
return await _context.Users
.FirstOrDefaultAsync(u => u.Email == email);
}
public async Task<bool> ExistsAsync(string id)
{
return await _context.Users.AnyAsync(u => u.Id == id);
}
}
}

View file

@ -0,0 +1,106 @@
using Data.SeaHavenIndustries;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Interfaces;
namespace SeaHaven.DataServices.Implementation
{
public class VendorDataService : IVendorDataService
{
private readonly ApplicationDbContext _context;
public VendorDataService(ApplicationDbContext context)
{
_context = context;
}
public async Task<Vendor?> GetByIdAsync(int id)
{
return await _context.Vendors.FindAsync(id);
}
public async Task<Vendor?> GetByIdWithDetailsAsync(int id)
{
return await _context.Vendors
.FirstOrDefaultAsync(v => v.Id == id);
}
public async Task<IEnumerable<Vendor>> GetAllAsync()
{
return await _context.Vendors.ToListAsync();
}
public async Task<(IEnumerable<Vendor> Items, int TotalCount)> GetPagedAsync(
int page,
int pageSize,
string? search = null)
{
search ??= "";
var query = _context.Vendors.Where(v => v.IsActive);
if (!string.IsNullOrWhiteSpace(search))
{
var s = search.ToLower();
query = query.Where(v =>
(v.CompanyName ?? "").ToLower().Contains(s) ||
(v.ContactName ?? "").ToLower().Contains(s) ||
(v.Email ?? "").ToLower().Contains(s) ||
(v.TradeSpecialties ?? "").ToLower().Contains(s));
}
var totalCount = await query.CountAsync();
var items = await query
.OrderByDescending(v => v.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return (items, totalCount);
}
public async Task<Vendor> AddAsync(Vendor vendor)
{
await _context.Vendors.AddAsync(vendor);
await _context.SaveChangesAsync();
return vendor;
}
public async Task UpdateAsync(Vendor vendor)
{
_context.Vendors.Update(vendor);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var entity = await GetByIdAsync(id);
if (entity != null)
{
_context.Vendors.Remove(entity);
await _context.SaveChangesAsync();
}
}
public async Task<bool> ExistsAsync(int id)
{
return await _context.Vendors.AnyAsync(v => v.Id == id);
}
public async Task<VendorAccessToken?> GetActiveTokenAsync(int vendorId)
{
return await _context.VendorAccessTokens
.FirstOrDefaultAsync(t => t.VendorId == vendorId);
}
public async Task<VendorAccessToken?> GetTokenByStringAsync(string token)
{
return await _context.VendorAccessTokens
.FirstOrDefaultAsync(t => t.Token == token);
}
public async Task<int> CountAsync()
{
return await _context.Vendors.CountAsync();
}
}
}

View file

@ -0,0 +1,367 @@
using Data.SeaHavenIndustries;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Interfaces;
namespace SeaHaven.DataServices.Implementation
{
public class WorkOrderDataService : IWorkOrderDataService
{
private readonly ApplicationDbContext _context;
public WorkOrderDataService(ApplicationDbContext context)
{
_context = context;
}
public async Task<WorkOrder?> GetByIdAsync(int id)
{
return await _context.workOrders.FindAsync(id);
}
public async Task<WorkOrder?> GetByIdWithDetailsAsync(int id)
{
return await _context.workOrders
.Include(w => w.Locations)
.Include(w => w.AssignToUser)
.FirstOrDefaultAsync(w => w.Id == id);
}
public async Task<WorkOrder?> GetByInternalNumberAsync(string internalNumber)
{
return await _context.workOrders
.FirstOrDefaultAsync(w => w.InternalWONumber == internalNumber);
}
public async Task<IEnumerable<WorkOrder>> GetAllAsync()
{
return await _context.workOrders.ToListAsync();
}
public async Task<IEnumerable<WorkOrder>> GetByLocationIdAsync(int locationId)
{
return await _context.workOrders
.Where(w => w.LocationId == locationId)
.ToListAsync();
}
public async Task<IEnumerable<WorkOrder>> GetByAssigneeAsync(string assignee)
{
return await _context.workOrders
.Where(w => w.AssignTo == assignee)
.ToListAsync();
}
public async Task<IEnumerable<WorkOrder>> GetByStatusAsync(string status)
{
return await _context.workOrders
.Where(w => w.Status == status)
.ToListAsync();
}
public async Task<IEnumerable<WorkOrder>> GetByPriorityAsync(string priority)
{
return await _context.workOrders
.Where(w => w.Priority == priority)
.ToListAsync();
}
public async Task<(IEnumerable<WorkOrder> Items, int TotalCount)> GetPagedAsync(
int page,
int pageSize,
string? search = null,
string? status = null,
string? priority = null)
{
var query = _context.workOrders.AsQueryable();
if (!string.IsNullOrWhiteSpace(search))
{
query = query.Where(w =>
w.WorkerOrderNumber!.Contains(search) ||
w.WorkerOrderTitle!.Contains(search));
}
if (!string.IsNullOrWhiteSpace(status))
{
query = query.Where(w => w.Status == status);
}
if (!string.IsNullOrWhiteSpace(priority))
{
query = query.Where(w => w.Priority == priority);
}
var totalCount = await query.CountAsync();
var items = await query
.OrderByDescending(w => w.CreatedDate)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return (items, totalCount);
}
public async Task<(IEnumerable<object> Items, int TotalCount)> GetWorkOrderListPagedAsync(
int page,
int pageSize,
string? search = null,
string? status = null,
string? assignee = null,
int? locationId = null,
string? sortBy = "lastUpdated",
string? sortDir = "desc")
{
search ??= "";
var query = _context.workOrders
.Include(w => w.Locations)
.Include(w => w.AssignToUser)
.Where(w => w.istemplate != true);
if (locationId.HasValue)
query = query.Where(w => w.LocationId == locationId.Value);
if (!string.IsNullOrWhiteSpace(status))
query = query.Where(w => w.Status == status);
if (assignee == "__unassigned")
query = query.Where(w => w.AssignTo == null || w.AssignTo == "");
else if (!string.IsNullOrWhiteSpace(assignee))
query = query.Where(w => w.AssignToUser != null && (w.AssignToUser.FirstName + " " + w.AssignToUser.LastName).Contains(assignee));
if (!string.IsNullOrWhiteSpace(search))
{
var s = search.ToLower();
query = query.Where(w =>
(w.InternalWONumber ?? "").ToLower().Contains(s) ||
(w.WorkerOrderNumber ?? "").ToLower().Contains(s) ||
(w.WorkerOrderTitle ?? "").ToLower().Contains(s) ||
(w.Locations.Title ?? "").ToLower().Contains(s) ||
(w.Locations.Name ?? "").ToLower().Contains(s)
);
}
var totalCount = await query.CountAsync();
var isAsc = sortDir?.ToLower() == "asc";
IOrderedQueryable<WorkOrder> ordered = (sortBy?.ToLower()) switch
{
"number" => isAsc ? query.OrderBy(w => w.InternalWONumber) : query.OrderByDescending(w => w.InternalWONumber),
"title" => isAsc ? query.OrderBy(w => w.WorkerOrderTitle) : query.OrderByDescending(w => w.WorkerOrderTitle),
"location" => isAsc ? query.OrderBy(w => w.Locations != null ? w.Locations.Name : "") : query.OrderByDescending(w => w.Locations != null ? w.Locations.Name : ""),
"priority" => isAsc ? query.OrderBy(w => w.Priority) : query.OrderByDescending(w => w.Priority),
"status" => isAsc ? query.OrderBy(w => w.Status) : query.OrderByDescending(w => w.Status),
"createddate" => isAsc ? query.OrderBy(w => w.CreatedDate) : query.OrderByDescending(w => w.CreatedDate),
"assignedto" => isAsc ? query.OrderBy(w => w.AssignToUser != null ? w.AssignToUser.FirstName : "") : query.OrderByDescending(w => w.AssignToUser != null ? w.AssignToUser.FirstName : ""),
"lastupdated" => isAsc ? query.OrderBy(w => w.LastModificationTime) : query.OrderByDescending(w => w.LastModificationTime),
_ => query.OrderByDescending(w => w.Id)
};
var data = await ordered
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(w => new
{
id = w.Id,
internalWONumber = w.InternalWONumber,
workOrderNumber = w.WorkerOrderNumber,
number = w.InternalWONumber ?? w.WorkerOrderNumber,
title = w.WorkerOrderTitle,
location = w.Locations != null ? w.Locations.Name : "",
locationName = w.Locations != null ? w.Locations.Name : "",
priority = w.Priority,
status = w.Status,
createdDate = w.CreatedDate,
dueDate = w.DueDate,
assignedTo = w.AssignToUser != null ? w.AssignToUser.FirstName + " " + w.AssignToUser.LastName : "",
employeeName = w.AssignToUser != null ? w.AssignToUser.FirstName + " " + w.AssignToUser.LastName : "",
lastUpdated = new[] {
w.LastModificationTime,
w.Comments.Max(c => (DateTime?)c.CreatedDate),
_context.WorkOrderAuditLogs.Where(a => a.WorkOrderId == w.Id).Max(a => (DateTime?)a.CreatedAt)
}.Where(d => d.HasValue).Max()
})
.ToListAsync();
return (data, totalCount);
}
public async Task<WorkOrder> AddAsync(WorkOrder workOrder)
{
workOrder.CreatedDate = DateTime.Now;
await _context.workOrders.AddAsync(workOrder);
await _context.SaveChangesAsync();
return workOrder;
}
public async Task UpdateAsync(WorkOrder workOrder)
{
workOrder.LastModificationTime = DateTime.Now;
_context.workOrders.Update(workOrder);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var entity = await GetByIdAsync(id);
if (entity != null)
{
_context.workOrders.Remove(entity);
await _context.SaveChangesAsync();
}
}
public async Task<IEnumerable<WorkOrderContacts>> GetWorkOrderContactsAsync(int workOrderId)
{
return await _context.WorkOrderContacts
.Where(wc => wc.WorkorderId == workOrderId)
.ToListAsync();
}
public async Task<IEnumerable<WorkOrderCategories>> GetWorkOrderCategoriesAsync(int workOrderId)
{
return await _context.workOrderCategories
.Where(wc => wc.WorkorderId == workOrderId)
.ToListAsync();
}
public async Task<IEnumerable<WorkOrderAttachments>> GetWorkOrderAttachmentsAsync(int workOrderId)
{
return await _context.workOrderAttachments
.Where(wa => wa.WorkorderId == workOrderId)
.ToListAsync();
}
public async Task<IEnumerable<Comments>> GetWorkOrderCommentsAsync(int workOrderId)
{
return await _context.Comments
.Where(c => c.WorkerOrderId == workOrderId)
.ToListAsync();
}
public async Task<IEnumerable<WorkOrderAuditLog>> GetWorkOrderAuditLogsAsync(int workOrderId)
{
return await _context.WorkOrderAuditLogs
.Where(a => a.WorkOrderId == workOrderId)
.OrderByDescending(a => a.CreatedAt)
.ToListAsync();
}
public async Task AddWorkOrderContactsAsync(int workOrderId, IEnumerable<int> contactIds)
{
var contacts = contactIds.Select(cid => new WorkOrderContacts
{
WorkorderId = workOrderId,
ContactId = cid
});
await _context.WorkOrderContacts.AddRangeAsync(contacts);
await _context.SaveChangesAsync();
}
public async Task AddWorkOrderCategoriesAsync(int workOrderId, IEnumerable<int> categoryIds)
{
var categories = categoryIds.Select(catId => new WorkOrderCategories
{
WorkorderId = workOrderId,
CategoryId = catId
});
await _context.workOrderCategories.AddRangeAsync(categories);
await _context.SaveChangesAsync();
}
public async Task AddWorkOrderAttachmentAsync(WorkOrderAttachments attachment)
{
await _context.workOrderAttachments.AddAsync(attachment);
await _context.SaveChangesAsync();
}
public async Task AddAuditLogAsync(WorkOrderAuditLog auditLog)
{
await _context.WorkOrderAuditLogs.AddAsync(auditLog);
await _context.SaveChangesAsync();
}
public async Task DeleteWorkOrderContactsAsync(int workOrderId)
{
var contacts = await _context.WorkOrderContacts
.Where(wc => wc.WorkorderId == workOrderId)
.ToListAsync();
if (contacts.Any())
{
_context.WorkOrderContacts.RemoveRange(contacts);
await _context.SaveChangesAsync();
}
}
public async Task DeleteWorkOrderCategoriesAsync(int workOrderId)
{
var categories = await _context.workOrderCategories
.Where(wc => wc.WorkorderId == workOrderId)
.ToListAsync();
if (categories.Any())
{
_context.workOrderCategories.RemoveRange(categories);
await _context.SaveChangesAsync();
}
}
public async Task DeleteWorkOrderAttachmentsAsync(int workOrderId)
{
var attachments = await _context.workOrderAttachments
.Where(wa => wa.WorkorderId == workOrderId)
.ToListAsync();
if (attachments.Any())
{
_context.workOrderAttachments.RemoveRange(attachments);
await _context.SaveChangesAsync();
}
}
public async Task<bool> ExistsAsync(int id)
{
return await _context.workOrders.AnyAsync(w => w.Id == id);
}
public async Task<bool> InternalNumberExistsAsync(string internalNumber, int? excludeId = null)
{
return await _context.workOrders.AnyAsync(w =>
w.InternalWONumber == internalNumber &&
(excludeId == null || w.Id != excludeId));
}
public async Task<string> GenerateNextInternalWONumberAsync()
{
return await Task.FromResult($"WO-{DateTime.Now:yyyyMMdd}-{new Random().Next(1000, 9999)}");
}
public async Task<int> CountAsync()
{
return await _context.workOrders.CountAsync();
}
public async Task<int> CountByStatusAsync(string status)
{
return await _context.workOrders.CountAsync(w => w.Status == status);
}
public async Task<int> CountByLocationAsync(int locationId)
{
return await _context.workOrders.CountAsync(w => w.LocationId == locationId);
}
public async Task<IEnumerable<WorkOrder>> GetAllWithDetailsAsync()
{
return await _context.workOrders
.Where(w => w.istemplate != true)
.Include(w => w.Locations)
.Include(w => w.AssignToUser)
.AsSplitQuery()
.ToListAsync();
}
}
}

View file

@ -0,0 +1,17 @@
using Data.SeaHavenIndustries;
namespace SeaHaven.DataServices.Interfaces
{
public interface IAccountDataService
{
Task<Accounts?> GetByIdAsync(int id);
Task<Accounts?> GetByIdWithDetailsAsync(int id);
Task<IEnumerable<Accounts>> GetAllAsync();
Task<(IEnumerable<Accounts> Items, int TotalCount)> GetPagedAsync(int page, int pageSize, string? search = null);
Task<Accounts> AddAsync(Accounts account);
Task UpdateAsync(Accounts account);
Task DeleteAsync(int id);
Task<bool> ExistsAsync(int id);
Task<int> CountAsync();
}
}

View file

@ -0,0 +1,18 @@
using Data.SeaHavenIndustries;
namespace SeaHaven.DataServices.Interfaces
{
public interface IAssetDataService
{
Task<Assets?> GetByIdAsync(int id);
Task<Assets?> GetByIdWithDetailsAsync(int id);
Task<IEnumerable<Assets>> GetAllAsync();
Task<IEnumerable<Assets>> GetByLocationIdAsync(int locationId);
Task<(IEnumerable<Assets> Items, int TotalCount)> GetPagedAsync(int page, int pageSize, string? search = null);
Task<Assets> AddAsync(Assets asset);
Task UpdateAsync(Assets asset);
Task DeleteAsync(int id);
Task<bool> ExistsAsync(int id);
Task<int> CountAsync();
}
}

View file

@ -0,0 +1,14 @@
using Data.SeaHavenIndustries;
namespace SeaHaven.DataServices.Interfaces
{
public interface ICategoryDataService
{
Task<Category?> GetByIdAsync(int id);
Task<IEnumerable<Category>> GetAllAsync();
Task<Category> AddAsync(Category category);
Task UpdateAsync(Category category);
Task DeleteAsync(int id);
Task<bool> ExistsAsync(int id);
}
}

View file

@ -0,0 +1,16 @@
using Data.SeaHavenIndustries;
namespace SeaHaven.DataServices.Interfaces
{
public interface ICommentDataService
{
Task<Comments?> GetByIdAsync(int id);
Task<IEnumerable<Comments>> GetAllAsync();
Task<IEnumerable<Comments>> GetByWorkOrderIdAsync(int workOrderId);
Task<IEnumerable<Comments>> GetByDispatchIdAsync(int dispatchId);
Task<Comments> AddAsync(Comments comment);
Task UpdateAsync(Comments comment);
Task DeleteAsync(int id);
Task<bool> ExistsAsync(int id);
}
}

View file

@ -0,0 +1,42 @@
using Data.SeaHavenIndustries;
namespace SeaHaven.DataServices.Interfaces
{
/// <summary>
/// Data service interface for Contact entity
/// Handles all data access operations for Contacts
/// </summary>
public interface IContactDataService
{
// Query operations
Task<Contacts?> GetByIdAsync(int id);
Task<Contacts?> GetByIdWithDetailsAsync(int id);
Task<IEnumerable<Contacts>> GetAllAsync();
Task<IEnumerable<Contacts>> GetByAccountIdAsync(int accountId);
Task<IEnumerable<Contacts>> GetByTypeAsync(string type);
Task<Contacts?> GetByEmailAsync(string email);
Task<Contacts?> GetByPhoneAsync(string phone);
// Paged results
Task<(IEnumerable<Contacts> Items, int TotalCount)> GetPagedAsync(
int page,
int pageSize,
string? search = null,
string? type = null);
// Command operations
Task<Contacts> AddAsync(Contacts contact);
Task UpdateAsync(Contacts contact);
Task DeleteAsync(int id);
// Validation operations
Task<bool> ExistsAsync(int id);
Task<bool> EmailExistsAsync(string email, int? excludeId = null);
Task<bool> PhoneExistsAsync(string phone, int? excludeId = null);
// Count operations
Task<int> CountAsync();
Task<int> CountByAccountIdAsync(int accountId);
Task<int> CountByTypeAsync(string type);
}
}

View file

@ -0,0 +1,32 @@
using Data.SeaHavenIndustries;
namespace SeaHaven.DataServices.Interfaces
{
public interface IDispatchDataService
{
Task<Dispatch?> GetByIdAsync(int id);
Task<Dispatch?> GetByIdWithDetailsAsync(int id);
Task<IEnumerable<Dispatch>> GetAllAsync();
Task<IEnumerable<Dispatch>> GetByVendorIdAsync(int vendorId);
Task<IEnumerable<Dispatch>> GetByWorkOrderIdAsync(int workOrderId);
Task<IEnumerable<Dispatch>> GetByWorkOrderIdsAsync(IEnumerable<int> workOrderIds);
Task<IEnumerable<Dispatch>> GetByStatusAsync(string status);
Task<(IEnumerable<Dispatch> Items, int TotalCount)> GetPagedAsync(int page, int pageSize, string? search = null, string? status = null);
Task<Dispatch> AddAsync(Dispatch dispatch);
Task UpdateAsync(Dispatch dispatch);
Task DeleteAsync(int id);
Task<bool> ExistsAsync(int id);
Task<string> GenerateNextDispatchNumberAsync();
Task<string> GenerateNextPONumberAsync();
Task<int> CountAsync();
Task<IEnumerable<DispatchChecklistItem>> GetChecklistItemsAsync(int dispatchId);
Task<DispatchChecklistItem?> GetChecklistItemByIdAsync(int id);
Task AddChecklistItemAsync(DispatchChecklistItem item);
Task UpdateChecklistItemAsync(DispatchChecklistItem item);
Task<IEnumerable<DispatchSignoff>> GetSignoffsAsync(int dispatchId);
Task AddSignoffAsync(DispatchSignoff signoff);
Task<bool> HasSignoffTypeAsync(int dispatchId, string signoffType);
Task<IEnumerable<DispatchWorkOrder>> GetDispatchWorkOrdersAsync(int dispatchId);
Task AddDispatchWorkOrderAsync(DispatchWorkOrder dispatchWorkOrder);
}
}

View file

@ -0,0 +1,17 @@
using Data.SeaHavenIndustries;
namespace SeaHaven.DataServices.Interfaces
{
public interface IEmployeeDataService
{
Task<Employee?> GetByIdAsync(int id);
Task<Employee?> GetByIdWithDetailsAsync(int id);
Task<IEnumerable<Employee>> GetAllAsync();
Task<(IEnumerable<Employee> Items, int TotalCount)> GetPagedAsync(int page, int pageSize, string? search = null);
Task<Employee> AddAsync(Employee employee);
Task UpdateAsync(Employee employee);
Task DeleteAsync(int id);
Task<bool> ExistsAsync(int id);
Task<int> CountAsync();
}
}

View file

@ -0,0 +1,19 @@
using Data.SeaHavenIndustries;
namespace SeaHaven.DataServices.Interfaces
{
public interface IFollowUpDataService
{
Task<FollowUps?> GetByIdAsync(int id);
Task<FollowUps?> GetByIdWithDetailsAsync(int id);
Task<IEnumerable<FollowUps>> GetAllAsync();
Task<IEnumerable<FollowUps>> GetByWorkOrderIdAsync(int workOrderId);
Task<IEnumerable<FollowUps>> GetByUserIdAsync(string userId);
Task<(IEnumerable<FollowUps> Items, int TotalCount)> GetPagedAsync(int page, int pageSize, string? search = null, string? status = null);
Task<FollowUps> AddAsync(FollowUps followUp);
Task UpdateAsync(FollowUps followUp);
Task DeleteAsync(int id);
Task<bool> ExistsAsync(int id);
Task<int> CountAsync();
}
}

View file

@ -0,0 +1,24 @@
using Data.SeaHavenIndustries;
namespace SeaHaven.DataServices.Interfaces
{
public interface ILocationDataService
{
Task<Locations?> GetByIdAsync(int id);
Task<Locations?> GetByIdWithDetailsAsync(int id);
Task<IEnumerable<Locations>> GetAllAsync();
Task<IEnumerable<Locations>> GetByAccountIdAsync(int accountId);
// Paged results returning DTOs
Task<(IEnumerable<Locations> Items, int TotalCount)> GetPagedAsync(int page, int pageSize, string? search = null);
// For addressbook endpoint - returns simplified DTO
Task<(IEnumerable<object> Items, int TotalCount)> GetAddressbookPagedAsync(int page, int pageSize, string? search = null);
Task<Locations> AddAsync(Locations location);
Task UpdateAsync(Locations location);
Task DeleteAsync(int id);
Task<bool> ExistsAsync(int id);
Task<int> CountAsync();
}
}

View file

@ -0,0 +1,19 @@
using Data.SeaHavenIndustries;
namespace SeaHaven.DataServices.Interfaces
{
public interface IPMScheduleDataService
{
Task<PMSchedules?> GetByIdAsync(int id);
Task<PMSchedules?> GetByIdWithDetailsAsync(int id);
Task<IEnumerable<PMSchedules>> GetAllAsync();
Task<IEnumerable<PMSchedules>> GetAllWithDetailsAsync();
Task<IEnumerable<PMSchedules>> GetByLocationIdAsync(int locationId);
Task<(IEnumerable<PMSchedules> Items, int TotalCount)> GetPagedAsync(int page, int pageSize, string? search = null);
Task<PMSchedules> AddAsync(PMSchedules pmSchedule);
Task UpdateAsync(PMSchedules pmSchedule);
Task DeleteAsync(int id);
Task<bool> ExistsAsync(int id);
Task<int> CountAsync();
}
}

View file

@ -0,0 +1,16 @@
using Data.SeaHavenIndustries;
namespace SeaHaven.DataServices.Interfaces
{
public interface IQuotesDataService
{
Task<Quotes?> GetByIdAsync(int id);
Task<IEnumerable<Quotes>> GetAllAsync();
Task<IEnumerable<Quotes>> GetByWorkOrderIdAsync(int workOrderId);
Task<Quotes> AddAsync(Quotes quote);
Task UpdateAsync(Quotes quote);
Task DeleteAsync(int id);
Task<bool> ExistsAsync(int id);
Task UnlinkFromWorkOrderAsync(int workOrderId);
}
}

View file

@ -0,0 +1,15 @@
using Data.SeaHavenIndustries;
namespace SeaHaven.DataServices.Interfaces
{
public interface ITaskListTemplateDataService
{
Task<TaskListTemplate?> GetByIdAsync(int id);
Task<TaskListTemplate?> GetByIdWithItemsAsync(int id);
Task<IEnumerable<TaskListTemplate>> GetAllAsync();
Task<TaskListTemplate> AddAsync(TaskListTemplate template);
Task UpdateAsync(TaskListTemplate template);
Task DeleteAsync(int id);
Task<bool> ExistsAsync(int id);
}
}

View file

@ -0,0 +1,12 @@
using Data.SeaHavenIndustries;
namespace SeaHaven.DataServices.Interfaces
{
public interface IUserDataService
{
Task<ApplicationUser?> GetByIdAsync(string id);
Task<IEnumerable<ApplicationUser>> GetAllAsync();
Task<ApplicationUser?> GetByEmailAsync(string email);
Task<bool> ExistsAsync(string id);
}
}

View file

@ -0,0 +1,19 @@
using Data.SeaHavenIndustries;
namespace SeaHaven.DataServices.Interfaces
{
public interface IVendorDataService
{
Task<Vendor?> GetByIdAsync(int id);
Task<Vendor?> GetByIdWithDetailsAsync(int id);
Task<IEnumerable<Vendor>> GetAllAsync();
Task<(IEnumerable<Vendor> Items, int TotalCount)> GetPagedAsync(int page, int pageSize, string? search = null);
Task<Vendor> AddAsync(Vendor vendor);
Task UpdateAsync(Vendor vendor);
Task DeleteAsync(int id);
Task<bool> ExistsAsync(int id);
Task<VendorAccessToken?> GetActiveTokenAsync(int vendorId);
Task<VendorAccessToken?> GetTokenByStringAsync(string token);
Task<int> CountAsync();
}
}

View file

@ -0,0 +1,72 @@
using Data.SeaHavenIndustries;
namespace SeaHaven.DataServices.Interfaces
{
public interface IWorkOrderDataService
{
// Query operations
Task<WorkOrder?> GetByIdAsync(int id);
Task<WorkOrder?> GetByIdWithDetailsAsync(int id);
Task<WorkOrder?> GetByInternalNumberAsync(string internalNumber);
Task<IEnumerable<WorkOrder>> GetAllAsync();
Task<IEnumerable<WorkOrder>> GetByLocationIdAsync(int locationId);
Task<IEnumerable<WorkOrder>> GetByAssigneeAsync(string assigneeId);
Task<IEnumerable<WorkOrder>> GetByStatusAsync(string status);
Task<IEnumerable<WorkOrder>> GetByPriorityAsync(string priority);
// Paged results
Task<(IEnumerable<WorkOrder> Items, int TotalCount)> GetPagedAsync(
int page,
int pageSize,
string? search = null,
string? status = null,
string? priority = null);
// Paged list with detailed information (for GetWorkOrderList endpoint)
Task<(IEnumerable<object> Items, int TotalCount)> GetWorkOrderListPagedAsync(
int page,
int pageSize,
string? search = null,
string? status = null,
string? assignee = null,
int? locationId = null,
string? sortBy = "lastUpdated",
string? sortDir = "desc");
// Command operations
Task<WorkOrder> AddAsync(WorkOrder workOrder);
Task UpdateAsync(WorkOrder workOrder);
Task DeleteAsync(int id);
// Related entities - Read
Task<IEnumerable<WorkOrderContacts>> GetWorkOrderContactsAsync(int workOrderId);
Task<IEnumerable<WorkOrderCategories>> GetWorkOrderCategoriesAsync(int workOrderId);
Task<IEnumerable<WorkOrderAttachments>> GetWorkOrderAttachmentsAsync(int workOrderId);
Task<IEnumerable<Comments>> GetWorkOrderCommentsAsync(int workOrderId);
Task<IEnumerable<WorkOrderAuditLog>> GetWorkOrderAuditLogsAsync(int workOrderId);
// Related entities - Write
Task AddWorkOrderContactsAsync(int workOrderId, IEnumerable<int> contactIds);
Task AddWorkOrderCategoriesAsync(int workOrderId, IEnumerable<int> categoryIds);
Task AddWorkOrderAttachmentAsync(WorkOrderAttachments attachment);
Task AddAuditLogAsync(WorkOrderAuditLog auditLog);
// Related entities - Delete
Task DeleteWorkOrderContactsAsync(int workOrderId);
Task DeleteWorkOrderCategoriesAsync(int workOrderId);
Task DeleteWorkOrderAttachmentsAsync(int workOrderId);
// Validation
Task<bool> ExistsAsync(int id);
Task<bool> InternalNumberExistsAsync(string internalNumber, int? excludeId = null);
// Internal number generation
Task<string> GenerateNextInternalWONumberAsync();
// Statistics
Task<int> CountAsync();
Task<int> CountByStatusAsync(string status);
Task<int> CountByLocationAsync(int locationId);
Task<IEnumerable<WorkOrder>> GetAllWithDetailsAsync();
}
}

View file

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Data.SeaHavenIndustries\Data.SeaHavenIndustries.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,34 @@
using FluentAssertions;
using SeaHaven.Services.DTOs;
using Xunit;
namespace SeaHaven.Services.Tests;
public class PagedResultTests
{
[Theory]
[InlineData(0, 10, 0)]
[InlineData(10, 10, 1)]
[InlineData(25, 10, 3)]
[InlineData(100, 25, 4)]
public void TotalPages_IsCeilingOfCountOverPageSize(int totalCount, int pageSize, int expectedPages)
{
var result = new PagedResult<string> { TotalCount = totalCount, PageSize = pageSize, Page = 1 };
result.TotalPages.Should().Be(expectedPages);
}
[Fact]
public void HasPrevious_IsFalseOnFirstPage_TrueAfterward()
{
new PagedResult<string> { Page = 1, PageSize = 10, TotalCount = 30 }.HasPrevious.Should().BeFalse();
new PagedResult<string> { Page = 2, PageSize = 10, TotalCount = 30 }.HasPrevious.Should().BeTrue();
}
[Fact]
public void HasNext_IsTrueBeforeLastPage_FalseOnLastPage()
{
new PagedResult<string> { Page = 2, PageSize = 10, TotalCount = 30 }.HasNext.Should().BeTrue();
new PagedResult<string> { Page = 3, PageSize = 10, TotalCount = 30 }.HasNext.Should().BeFalse();
}
}

View file

@ -0,0 +1,23 @@
<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="FluentAssertions" Version="6.12.1" />
<PackageReference Include="FluentValidation" Version="12.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SeaHaven.Services\SeaHaven.Services.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,104 @@
using FluentAssertions;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Validation;
using Xunit;
namespace SeaHaven.Services.Tests;
public class CreateAssetValidationTests
{
private readonly CreateAssetValidation _validator = new();
[Fact]
public void Validate_WhenNameEmpty_Fails()
{
var result = _validator.Validate(new CreateAssetDTO { Name = "" });
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.PropertyName == nameof(CreateAssetDTO.Name));
}
[Fact]
public void Validate_WhenNameTooLong_Fails()
{
var result = _validator.Validate(new CreateAssetDTO { Name = new string('a', 201) });
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.PropertyName == nameof(CreateAssetDTO.Name));
}
[Fact]
public void Validate_WhenLocationIdZero_Fails()
{
var result = _validator.Validate(new CreateAssetDTO { Name = "Pump", LocationId = 0 });
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.PropertyName == nameof(CreateAssetDTO.LocationId));
}
[Fact]
public void Validate_WhenValid_Passes()
{
var result = _validator.Validate(new CreateAssetDTO { Name = "Pump", LocationId = 5 });
result.IsValid.Should().BeTrue();
}
}
public class CreateContactValidationTests
{
private readonly CreateContactValidation _validator = new();
private static CreateContactDTO Valid() => new()
{
FirstName = "Ada",
LastName = "Lovelace",
Email = "ada@example.com"
};
[Fact]
public void Validate_WhenNamesMissing_Fails()
{
var dto = Valid();
dto.FirstName = "";
dto.LastName = "";
var result = _validator.Validate(dto);
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.PropertyName == nameof(CreateContactDTO.FirstName));
result.Errors.Should().Contain(e => e.PropertyName == nameof(CreateContactDTO.LastName));
}
[Fact]
public void Validate_WhenEmailMalformed_Fails()
{
var dto = Valid();
dto.Email = "not-an-email";
var result = _validator.Validate(dto);
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.PropertyName == nameof(CreateContactDTO.Email));
}
[Fact]
public void Validate_WhenPhoneTooLong_Fails()
{
var dto = Valid();
dto.Phone = new string('1', 21);
var result = _validator.Validate(dto);
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.PropertyName == nameof(CreateContactDTO.Phone));
}
[Fact]
public void Validate_WhenValid_Passes()
{
var result = _validator.Validate(Valid());
result.IsValid.Should().BeTrue();
}
}

View file

@ -0,0 +1,74 @@
using Data.SeaHavenIndustries;
using Data.SeaHavenIndustries.Enums;
using FluentAssertions;
using SeaHaven.Services.DTOs;
using Xunit;
namespace SeaHaven.Services.Tests;
public class WorkOrderMappingTests
{
[Fact]
public void MapToModel_WithValidEnumStrings_ParsesStatusAndPriority()
{
var entity = new WorkOrder
{
Id = 7,
WorkerOrderTitle = "Fix pump",
Status = "InProgress",
Priority = "High"
};
var dto = WorkOrderDTO.MapToModel(entity);
dto.Id.Should().Be(7);
dto.WorkerOrderTitle.Should().Be("Fix pump");
dto.Status.Should().Be(WorkOrderStatus.InProgress);
dto.Priority.Should().Be(WorkOrderPriority.High);
}
[Fact]
public void MapToModel_WithUnparseableEnumStrings_FallsBackToDefaults()
{
var entity = new WorkOrder
{
WorkerOrderTitle = "Garbage",
Status = "not-a-status",
Priority = "not-a-priority"
};
var dto = WorkOrderDTO.MapToModel(entity);
dto.Status.Should().Be(WorkOrderStatus.Open);
dto.Priority.Should().Be(WorkOrderPriority.Medium);
}
[Fact]
public void MapToEntity_SerializesEnumsToStrings()
{
var dto = new WorkOrderDTO
{
Id = 3,
WorkerOrderTitle = "Inspect",
Status = WorkOrderStatus.Completed,
Priority = WorkOrderPriority.Critical
};
var entity = WorkOrderDTO.MapToEntity(dto);
entity.Status.Should().Be("Completed");
entity.Priority.Should().Be("Critical");
}
[Fact]
public void CreateMapToEntity_WhenStatusAndPriorityNull_AppliesDefaults()
{
var dto = new CreateWorkOrderDTO { WorkerOrderTitle = "New" };
var entity = CreateWorkOrderDTO.MapToEntity(dto);
entity.Status.Should().Be("Open");
entity.Priority.Should().Be("Medium");
entity.CreatedDate.Should().NotBeNull();
}
}

View file

@ -0,0 +1,30 @@
namespace SeaHaven.Services.DTOs
{
public class AccountDTO
{
public int Id { get; set; }
public string? Name { get; set; }
public string? Type { get; set; }
public string? Phone { get; set; }
public string? Email { get; set; }
public string? Website { get; set; }
public DateTime? CreatedDate { get; set; }
public string? CreatedBy { get; set; }
}
public class CreateAccountDTO
{
public required string Name { get; set; }
public string? Phone { get; set; }
public string? Email { get; set; }
public string? Website { get; set; }
}
public class UpdateAccountDTO
{
public string? Name { get; set; }
public string? Phone { get; set; }
public string? Email { get; set; }
public string? Website { get; set; }
}
}

View file

@ -0,0 +1,23 @@
namespace SeaHaven.Services.DTOs
{
public class AssetDTO
{
public int Id { get; set; }
public string? Name { get; set; }
public int? LocationId { get; set; }
public string? LocationName { get; set; }
public DateTime? CreatedDate { get; set; }
}
public class CreateAssetDTO
{
public required string Name { get; set; }
public int? LocationId { get; set; }
}
public class UpdateAssetDTO
{
public string? Name { get; set; }
public int? LocationId { get; set; }
}
}

View file

@ -0,0 +1,55 @@
namespace SeaHaven.Services.DTOs
{
public class ContactDTO
{
public int Id { get; set; }
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string? Email { get; set; }
public string? Phone { get; set; }
public string? Mobile { get; set; }
public string? Title { get; set; }
public string? Type { get; set; }
public int? AccountId { get; set; }
public string? AccountName { get; set; }
public DateTime? CreatedDate { get; set; }
public string? CreatedBy { get; set; }
public DateTime? ModifiedDate { get; set; }
public string? ModifiedBy { get; set; }
}
public class CreateContactDTO
{
public required string FirstName { get; set; }
public required string LastName { get; set; }
public required string Email { get; set; }
public string? Phone { get; set; }
public string? Mobile { get; set; }
public string? Title { get; set; }
public string? Type { get; set; }
public int? AccountId { get; set; }
}
public class UpdateContactDTO
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string? Email { get; set; }
public string? Phone { get; set; }
public string? Mobile { get; set; }
public string? Title { get; set; }
public string? Type { get; set; }
public int? AccountId { get; set; }
}
public class PagedResult<T>
{
public IEnumerable<T> Items { get; set; } = new List<T>();
public int TotalCount { get; set; }
public int Page { get; set; }
public int PageSize { get; set; }
public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize);
public bool HasPrevious => Page > 1;
public bool HasNext => Page < TotalPages;
}
}

View file

@ -0,0 +1,30 @@
namespace SeaHaven.Services.DTOs
{
public class DispatchDTO
{
public int Id { get; set; }
public string? DispatchNumber { get; set; }
public string? Status { get; set; }
public int? VendorId { get; set; }
public string? VendorName { get; set; }
public DateTime? ScheduledDate { get; set; }
public DateTime? CompletedDate { get; set; }
public DateTime? CreatedDate { get; set; }
public string? CreatedBy { get; set; }
}
public class CreateDispatchDTO
{
public required int VendorId { get; set; }
public DateTime? ScheduledDate { get; set; }
public string? Status { get; set; }
}
public class UpdateDispatchDTO
{
public int? VendorId { get; set; }
public DateTime? ScheduledDate { get; set; }
public DateTime? CompletedDate { get; set; }
public string? Status { get; set; }
}
}

View file

@ -0,0 +1,40 @@
namespace SeaHaven.Services.DTOs
{
public class EmployeeDTO
{
public int Id { get; set; }
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string? Email { get; set; }
public string? Phone { get; set; }
public int? DepartmentId { get; set; }
public string? DepartmentName { get; set; }
public int? JobTitleId { get; set; }
public string? JobTitleName { get; set; }
public int? RegionId { get; set; }
public string? RegionName { get; set; }
public DateTime? CreatedDate { get; set; }
}
public class CreateEmployeeDTO
{
public required string FirstName { get; set; }
public required string LastName { get; set; }
public string? Email { get; set; }
public string? Phone { get; set; }
public int? DepartmentId { get; set; }
public int? JobTitleId { get; set; }
public int? RegionId { get; set; }
}
public class UpdateEmployeeDTO
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string? Email { get; set; }
public string? Phone { get; set; }
public int? DepartmentId { get; set; }
public int? JobTitleId { get; set; }
public int? RegionId { get; set; }
}
}

View file

@ -0,0 +1,43 @@
namespace SeaHaven.Services.DTOs
{
public class FollowUpDTO
{
public int Id { get; set; }
public string? Reason { get; set; }
public DateTime ScheduleDate { get; set; }
public TimeSpan ScheduleStartTime { get; set; }
public string? Status { get; set; }
public int? EmployeeId { get; set; }
public string? EmployeeName { get; set; }
public int? WorkOrderId { get; set; }
public int? AccountId { get; set; }
public string? AccountName { get; set; }
public int? LocationId { get; set; }
public string? LocationName { get; set; }
public DateTime? CreatedDate { get; set; }
}
public class CreateFollowUpDTO
{
public required string Reason { get; set; }
public DateTime ScheduleDate { get; set; }
public TimeSpan ScheduleStartTime { get; set; }
public required string Status { get; set; }
public int? EmployeeId { get; set; }
public int? WorkOrderId { get; set; }
public int? AccountId { get; set; }
public int? LocationId { get; set; }
}
public class UpdateFollowUpDTO
{
public string? Reason { get; set; }
public DateTime? ScheduleDate { get; set; }
public TimeSpan? ScheduleStartTime { get; set; }
public string? Status { get; set; }
public int? EmployeeId { get; set; }
public int? WorkOrderId { get; set; }
public int? AccountId { get; set; }
public int? LocationId { get; set; }
}
}

View file

@ -0,0 +1,39 @@
namespace SeaHaven.Services.DTOs
{
public class LocationDTO
{
public int Id { get; set; }
public string? Name { get; set; }
public string? Title { get; set; }
public string? Address1 { get; set; }
public string? Address2 { get; set; }
public string? City { get; set; }
public string? State { get; set; }
public string? Zip { get; set; }
public string? PhoneNumber { get; set; }
public string? Email { get; set; }
public string? Status { get; set; }
public DateTime? CreatedDate { get; set; }
public string? CreatedBy { get; set; }
}
public class CreateLocationDTO
{
public required string LocationName { get; set; }
public string? Address { get; set; }
public string? City { get; set; }
public string? State { get; set; }
public string? Zipcode { get; set; }
public int? AccountId { get; set; }
}
public class UpdateLocationDTO
{
public string? LocationName { get; set; }
public string? Address { get; set; }
public string? City { get; set; }
public string? State { get; set; }
public string? Zipcode { get; set; }
public int? AccountId { get; set; }
}
}

View file

@ -0,0 +1,36 @@
namespace SeaHaven.Services.DTOs
{
public class PMScheduleDTO
{
public int Id { get; set; }
public string? Name { get; set; }
public string? Frequency { get; set; }
public DateTime StartDate { get; set; }
public bool IsActive { get; set; }
public int? AssetId { get; set; }
public string? AssetName { get; set; }
public int? LocationId { get; set; }
public string? LocationName { get; set; }
public DateTime? CreatedDate { get; set; }
}
public class CreatePMScheduleDTO
{
public required string Name { get; set; }
public required string Frequency { get; set; }
public DateTime StartDate { get; set; }
public bool IsActive { get; set; }
public int? AssetId { get; set; }
public int? LocationId { get; set; }
}
public class UpdatePMScheduleDTO
{
public string? Name { get; set; }
public string? Frequency { get; set; }
public DateTime? StartDate { get; set; }
public bool? IsActive { get; set; }
public int? AssetId { get; set; }
public int? LocationId { get; set; }
}
}

View file

@ -0,0 +1,36 @@
namespace SeaHaven.Services.DTOs
{
public class VendorDTO
{
public int Id { get; set; }
public string? Name { get; set; }
public string? Email { get; set; }
public string? Phone { get; set; }
public string? Address { get; set; }
public string? City { get; set; }
public string? State { get; set; }
public string? Zipcode { get; set; }
}
public class CreateVendorDTO
{
public required string Name { get; set; }
public string? Email { get; set; }
public string? Phone { get; set; }
public string? Address { get; set; }
public string? City { get; set; }
public string? State { get; set; }
public string? Zipcode { get; set; }
}
public class UpdateVendorDTO
{
public string? Name { get; set; }
public string? Email { get; set; }
public string? Phone { get; set; }
public string? Address { get; set; }
public string? City { get; set; }
public string? State { get; set; }
public string? Zipcode { get; set; }
}
}

View file

@ -0,0 +1,129 @@
using Data.SeaHavenIndustries;
using Data.SeaHavenIndustries.Enums;
namespace SeaHaven.Services.DTOs
{
public class WorkOrderDTO
{
public int Id { get; set; }
public string? InternalWONumber { get; set; }
public string? WorkerOrderTitle { get; set; }
public string? Description { get; set; }
public WorkOrderStatus Status { get; set; }
public WorkOrderPriority Priority { get; set; }
public int? LocationId { get; set; }
public string? LocationName { get; set; }
public string? AssignTo { get; set; }
public DateTime? DueDate { get; set; }
public DateTime? CompletedDate { get; set; }
public DateTime? CreatedDate { get; set; }
public string? CreatedBy { get; set; }
public DateTime? ModifiedDate { get; set; }
public string? ModifiedBy { get; set; }
/// <summary>
/// Maps entity to DTO (Entity -> DTO)
/// </summary>
public static WorkOrderDTO MapToModel(WorkOrder entity)
{
if (entity == null) return null;
return new WorkOrderDTO
{
Id = entity.Id,
InternalWONumber = entity.InternalWONumber,
WorkerOrderTitle = entity.WorkerOrderTitle,
Description = entity.Description,
Status = Enum.TryParse<WorkOrderStatus>(entity.Status, out var status) ? status : WorkOrderStatus.Open,
Priority = Enum.TryParse<WorkOrderPriority>(entity.Priority, out var priority) ? priority : WorkOrderPriority.Medium,
LocationId = entity.LocationId,
LocationName = null,
AssignTo = entity.AssignTo,
DueDate = entity.DueDate,
CompletedDate = entity.CompletedDate,
CreatedDate = entity.CreatedDate,
CreatedBy = entity.createdby,
ModifiedDate = entity.LastModificationTime,
ModifiedBy = entity.LastModifierUserId?.ToString()
};
}
/// <summary>
/// Maps DTO to entity (DTO -> Entity)
/// </summary>
public static WorkOrder MapToEntity(WorkOrderDTO dto)
{
if (dto == null) return null;
return new WorkOrder
{
Id = dto.Id,
InternalWONumber = dto.InternalWONumber,
WorkerOrderTitle = dto.WorkerOrderTitle,
Description = dto.Description,
Status = dto.Status.ToString(),
Priority = dto.Priority.ToString(),
LocationId = dto.LocationId,
AssignTo = dto.AssignTo,
DueDate = dto.DueDate,
CompletedDate = dto.CompletedDate
};
}
}
public class CreateWorkOrderDTO
{
public required string WorkerOrderTitle { get; set; }
public string? Description { get; set; }
public WorkOrderStatus? Status { get; set; }
public WorkOrderPriority? Priority { get; set; }
public int? LocationId { get; set; }
public string? AssignTo { get; set; }
public DateTime? DueDate { get; set; }
public static WorkOrder MapToEntity(CreateWorkOrderDTO dto)
{
if (dto == null) return null!;
return new WorkOrder
{
WorkerOrderTitle = dto.WorkerOrderTitle,
Description = dto.Description,
Status = (dto.Status ?? WorkOrderStatus.Open).ToString(),
Priority = (dto.Priority ?? WorkOrderPriority.Medium).ToString(),
LocationId = dto.LocationId,
AssignTo = dto.AssignTo,
DueDate = dto.DueDate,
CreatedDate = DateTime.UtcNow
};
}
}
public class UpdateWorkOrderDTO
{
public string? WorkerOrderTitle { get; set; }
public string? Description { get; set; }
public WorkOrderStatus? Status { get; set; }
public WorkOrderPriority? Priority { get; set; }
public int? LocationId { get; set; }
public string? AssignTo { get; set; }
public DateTime? DueDate { get; set; }
public DateTime? CompletedDate { get; set; }
public static void UpdateEntity(UpdateWorkOrderDTO dto, WorkOrder entity)
{
if (dto == null || entity == null) return;
if (dto.WorkerOrderTitle != null) entity.WorkerOrderTitle = dto.WorkerOrderTitle;
if (dto.Description != null) entity.Description = dto.Description;
if (dto.Status.HasValue) entity.Status = dto.Status.ToString()!;
if (dto.Priority.HasValue) entity.Priority = dto.Priority.ToString()!;
if (dto.LocationId.HasValue) entity.LocationId = dto.LocationId;
if (dto.AssignTo != null) entity.AssignTo = dto.AssignTo;
if (dto.DueDate.HasValue) entity.DueDate = dto.DueDate;
if (dto.CompletedDate.HasValue) entity.CompletedDate = dto.CompletedDate;
entity.LastModificationTime = DateTime.UtcNow;
}
}
}

View file

@ -0,0 +1,38 @@
using FluentValidation;
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
namespace SeaHaven.Services.DependencyInjection
{
public static class ServicesModule
{
private static readonly List<Type> ExcludedTypes = new();
public static IServiceCollection AddBusinessServices(this IServiceCollection services)
{
var assembly = Assembly.GetExecutingAssembly();
var allClasses = assembly.GetTypes()
.Where(t => t.IsClass &&
!t.IsAbstract &&
!t.IsGenericType &&
!ExcludedTypes.Contains(t))
.ToList();
foreach (var implementationType in allClasses)
{
var defaultInterface = implementationType.GetInterfaces()
.FirstOrDefault(i => i.Name == $"I{implementationType.Name}");
if (defaultInterface != null)
{
services.AddScoped(defaultInterface, implementationType);
}
}
services.AddValidatorsFromAssembly(assembly);
return services;
}
}
}

View file

@ -0,0 +1,145 @@
using Data.SeaHavenIndustries;
using FluentValidation;
using SeaHaven.DataServices.Interfaces;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Interfaces;
using SeaHaven.Services.Validation;
namespace SeaHaven.Services.Implementation
{
/// <summary>
/// Account service implementation
/// Contains business logic, validation, and orchestration for Account operations
/// </summary>
public class AccountService : IAccountService
{
private readonly IAccountDataService _accountDataService;
private readonly ICreateAccountValidation _createValidator;
private readonly IUpdateAccountValidation _updateValidator;
public AccountService(
IAccountDataService accountDataService,
ICreateAccountValidation createValidator,
IUpdateAccountValidation updateValidator)
{
_accountDataService = accountDataService;
_createValidator = createValidator;
_updateValidator = updateValidator;
}
public async Task<AccountDTO?> GetAccountByIdAsync(int id)
{
var account = await _accountDataService.GetByIdAsync(id);
return account == null ? null : MapToDTO(account);
}
public async Task<AccountDTO?> GetAccountByIdWithDetailsAsync(int id)
{
var account = await _accountDataService.GetByIdWithDetailsAsync(id);
return account == null ? null : MapToDTO(account);
}
public async Task<IEnumerable<AccountDTO>> GetAllAccountsAsync()
{
var accounts = await _accountDataService.GetAllAsync();
return accounts.Select(MapToDTO);
}
public async Task<PagedResult<AccountDTO>> GetAccountsPagedAsync(int page, int pageSize, string? search = null)
{
var (items, totalCount) = await _accountDataService.GetPagedAsync(page, pageSize, search);
return new PagedResult<AccountDTO>
{
Items = items.Select(MapToDTO).ToList(),
TotalCount = totalCount,
Page = page,
PageSize = pageSize
};
}
public async Task<AccountDTO> CreateAccountAsync(CreateAccountDTO dto, string userId)
{
var validationResult = await _createValidator.ValidateAsync(dto);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
var account = new Accounts
{
Name = dto.Name,
Phone = dto.Phone,
Email = dto.Email,
Website = dto.Website,
CreatedDate = DateTime.UtcNow,
createdby = userId
};
var created = await _accountDataService.AddAsync(account);
return MapToDTO(created);
}
public async Task<AccountDTO> UpdateAccountAsync(int id, UpdateAccountDTO dto, string userId)
{
var validationResult = await _updateValidator.ValidateAsync(dto);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
var account = await _accountDataService.GetByIdAsync(id);
if (account == null)
throw new InvalidOperationException($"Account with ID {id} not found");
if (dto.Name != null) account.Name = dto.Name;
if (dto.Phone != null) account.Phone = dto.Phone;
if (dto.Email != null) account.Email = dto.Email;
if (dto.Website != null) account.Website = dto.Website;
account.LastModificationTime = DateTime.UtcNow;
if (int.TryParse(userId, out int userIdInt))
account.LastModifierUserId = userIdInt;
await _accountDataService.UpdateAsync(account);
return MapToDTO(account);
}
public async Task DeleteAccountAsync(int id, string userId)
{
var account = await _accountDataService.GetByIdAsync(id);
if (account == null)
throw new InvalidOperationException($"Account with ID {id} not found");
account.IsDeleted = true;
account.DeleterUserId = userId;
account.DeletionTime = DateTime.UtcNow;
await _accountDataService.UpdateAsync(account);
}
public async Task<bool> AccountExistsAsync(int id)
{
return await _accountDataService.ExistsAsync(id);
}
public async Task<int> GetTotalAccountCountAsync()
{
return await _accountDataService.CountAsync();
}
private AccountDTO MapToDTO(Accounts account)
{
return new AccountDTO
{
Id = account.Id,
Name = account.Name,
Type = account.Type,
Phone = account.Phone,
Email = account.Email,
Website = account.Website,
CreatedDate = account.CreatedDate,
CreatedBy = account.createdby
};
}
}
}

View file

@ -0,0 +1,144 @@
using Data.SeaHavenIndustries;
using FluentValidation;
using SeaHaven.DataServices.Interfaces;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Interfaces;
using SeaHaven.Services.Validation;
namespace SeaHaven.Services.Implementation
{
/// <summary>
/// Asset service implementation
/// Contains business logic, validation, and orchestration for Asset operations
/// </summary>
public class AssetService : IAssetService
{
private readonly IAssetDataService _assetDataService;
private readonly ICreateAssetValidation _createValidator;
private readonly IUpdateAssetValidation _updateValidator;
public AssetService(
IAssetDataService assetDataService,
ICreateAssetValidation createValidator,
IUpdateAssetValidation updateValidator)
{
_assetDataService = assetDataService;
_createValidator = createValidator;
_updateValidator = updateValidator;
}
public async Task<AssetDTO?> GetAssetByIdAsync(int id)
{
var asset = await _assetDataService.GetByIdAsync(id);
return asset == null ? null : MapToDTO(asset);
}
public async Task<AssetDTO?> GetAssetByIdWithDetailsAsync(int id)
{
var asset = await _assetDataService.GetByIdWithDetailsAsync(id);
return asset == null ? null : MapToDTO(asset);
}
public async Task<IEnumerable<AssetDTO>> GetAllAssetsAsync()
{
var assets = await _assetDataService.GetAllAsync();
return assets.Select(MapToDTO);
}
public async Task<IEnumerable<AssetDTO>> GetAssetsByLocationIdAsync(int locationId)
{
var assets = await _assetDataService.GetAllAsync();
return assets.Where(a => a.LocationId == locationId).Select(MapToDTO);
}
public async Task<PagedResult<AssetDTO>> GetAssetsPagedAsync(int page, int pageSize, string? search = null)
{
var (items, totalCount) = await _assetDataService.GetPagedAsync(page, pageSize, search);
return new PagedResult<AssetDTO>
{
Items = items.Select(MapToDTO).ToList(),
TotalCount = totalCount,
Page = page,
PageSize = pageSize
};
}
public async Task<AssetDTO> CreateAssetAsync(CreateAssetDTO dto, string userId)
{
var validationResult = await _createValidator.ValidateAsync(dto);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
var asset = new Assets
{
Name = dto.Name,
LocationId = dto.LocationId,
CreatedDate = DateTime.UtcNow,
createdby = userId
};
var created = await _assetDataService.AddAsync(asset);
return MapToDTO(created);
}
public async Task<AssetDTO> UpdateAssetAsync(int id, UpdateAssetDTO dto, string userId)
{
var validationResult = await _updateValidator.ValidateAsync(dto);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
var asset = await _assetDataService.GetByIdAsync(id);
if (asset == null)
throw new InvalidOperationException($"Asset with ID {id} not found");
if (dto.Name != null) asset.Name = dto.Name;
if (dto.LocationId.HasValue) asset.LocationId = dto.LocationId;
asset.LastModificationTime = DateTime.UtcNow;
if (int.TryParse(userId, out int userIdInt))
asset.LastModifierUserId = userIdInt;
await _assetDataService.UpdateAsync(asset);
return MapToDTO(asset);
}
public async Task DeleteAssetAsync(int id, string userId)
{
var asset = await _assetDataService.GetByIdAsync(id);
if (asset == null)
throw new InvalidOperationException($"Asset with ID {id} not found");
asset.IsDeleted = true;
asset.DeleterUserId = userId;
asset.DeletionTime = DateTime.UtcNow;
await _assetDataService.UpdateAsync(asset);
}
public async Task<bool> AssetExistsAsync(int id)
{
return await _assetDataService.ExistsAsync(id);
}
public async Task<int> GetTotalAssetCountAsync()
{
return await _assetDataService.CountAsync();
}
private AssetDTO MapToDTO(Assets asset)
{
return new AssetDTO
{
Id = asset.Id,
Name = asset.Name,
LocationId = asset.LocationId,
LocationName = asset.Location?.Name,
CreatedDate = asset.CreatedDate
};
}
}
}

View file

@ -0,0 +1,231 @@
using Data.SeaHavenIndustries;
using FluentValidation;
using SeaHaven.DataServices.Interfaces;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Interfaces;
using SeaHaven.Services.Validation;
namespace SeaHaven.Services.Implementation
{
/// <summary>
/// Contact service implementation
/// Contains business logic, validation, and orchestration for Contact operations
/// </summary>
public class ContactService : IContactService
{
private readonly IContactDataService _contactDataService;
private readonly ICreateContactValidation _createValidator;
private readonly IUpdateContactValidation _updateValidator;
public ContactService(
IContactDataService contactDataService,
ICreateContactValidation createValidator,
IUpdateContactValidation updateValidator)
{
_contactDataService = contactDataService;
_createValidator = createValidator;
_updateValidator = updateValidator;
}
// Query operations
public async Task<ContactDTO?> GetContactByIdAsync(int id)
{
var contact = await _contactDataService.GetByIdAsync(id);
return contact == null ? null : MapToDTO(contact);
}
public async Task<ContactDTO?> GetContactByIdWithDetailsAsync(int id)
{
var contact = await _contactDataService.GetByIdWithDetailsAsync(id);
return contact == null ? null : MapToDTO(contact);
}
public async Task<IEnumerable<ContactDTO>> GetAllContactsAsync()
{
var contacts = await _contactDataService.GetAllAsync();
return contacts.Select(MapToDTO);
}
public async Task<IEnumerable<ContactDTO>> GetContactsByAccountIdAsync(int accountId)
{
var contacts = await _contactDataService.GetByAccountIdAsync(accountId);
return contacts.Select(MapToDTO);
}
public async Task<IEnumerable<ContactDTO>> GetContactsByTypeAsync(string type)
{
var contacts = await _contactDataService.GetByTypeAsync(type);
return contacts.Select(MapToDTO);
}
public async Task<ContactDTO?> GetContactByEmailAsync(string email)
{
var contact = await _contactDataService.GetByEmailAsync(email);
return contact == null ? null : MapToDTO(contact);
}
// Paged results
public async Task<PagedResult<ContactDTO>> GetContactsPagedAsync(
int page,
int pageSize,
string? search = null,
string? type = null)
{
var (items, totalCount) = await _contactDataService.GetPagedAsync(page, pageSize, search, type);
return new PagedResult<ContactDTO>
{
Items = items.Select(MapToDTO),
TotalCount = totalCount,
Page = page,
PageSize = pageSize
};
}
// Command operations
public async Task<ContactDTO> CreateContactAsync(CreateContactDTO dto, string userId)
{
var validationResult = await _createValidator.ValidateAsync(dto);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
if (await _contactDataService.EmailExistsAsync(dto.Email))
throw new InvalidOperationException($"Email '{dto.Email}' already exists");
var contact = new Contacts
{
FirstName = dto.FirstName,
LastName = dto.LastName,
Email = dto.Email,
PhoneNumber = dto.Phone ?? dto.Mobile,
Title = dto.Title,
ContactType = dto.Type,
AccountId = dto.AccountId,
createdby = userId,
CreatedDate = DateTime.Now
};
var savedContact = await _contactDataService.AddAsync(contact);
return MapToDTO(savedContact);
}
public async Task<ContactDTO> UpdateContactAsync(int id, UpdateContactDTO dto, string userId)
{
var validationResult = await _updateValidator.ValidateAsync(dto);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
var existingContact = await _contactDataService.GetByIdAsync(id);
if (existingContact == null)
throw new KeyNotFoundException($"Contact with ID {id} not found");
if (!string.IsNullOrWhiteSpace(dto.Email) && dto.Email != existingContact.Email)
{
if (await _contactDataService.EmailExistsAsync(dto.Email, id))
throw new InvalidOperationException($"Email '{dto.Email}' already exists");
}
if (!string.IsNullOrWhiteSpace(dto.FirstName))
existingContact.FirstName = dto.FirstName;
if (!string.IsNullOrWhiteSpace(dto.LastName))
existingContact.LastName = dto.LastName;
if (!string.IsNullOrWhiteSpace(dto.Email))
existingContact.Email = dto.Email;
if (dto.Phone != null || dto.Mobile != null)
existingContact.PhoneNumber = dto.Phone ?? dto.Mobile;
if (dto.Title != null)
existingContact.Title = dto.Title;
if (dto.Type != null)
existingContact.ContactType = dto.Type;
if (dto.AccountId.HasValue)
existingContact.AccountId = dto.AccountId;
existingContact.LastModificationTime = DateTime.Now;
await _contactDataService.UpdateAsync(existingContact);
return MapToDTO(existingContact);
}
public async Task DeleteContactAsync(int id, string userId)
{
// Check if exists
if (!await _contactDataService.ExistsAsync(id))
throw new KeyNotFoundException($"Contact with ID {id} not found");
// Business logic: Check if contact is being used (add your logic here)
// For example, check if contact is linked to work orders, etc.
await _contactDataService.DeleteAsync(id);
}
// Validation operations
public async Task<bool> ContactExistsAsync(int id)
{
return await _contactDataService.ExistsAsync(id);
}
public async Task<bool> EmailExistsAsync(string email, int? excludeId = null)
{
return await _contactDataService.EmailExistsAsync(email, excludeId);
}
public async Task<bool> PhoneExistsAsync(string phone, int? excludeId = null)
{
return await _contactDataService.PhoneExistsAsync(phone, excludeId);
}
// Statistics
public async Task<int> GetTotalContactCountAsync()
{
return await _contactDataService.CountAsync();
}
public async Task<int> GetContactCountByAccountAsync(int accountId)
{
return await _contactDataService.CountByAccountIdAsync(accountId);
}
public async Task<Dictionary<string, int>> GetContactCountByTypeAsync()
{
var contacts = await _contactDataService.GetAllAsync();
return contacts
.Where(c => !string.IsNullOrWhiteSpace(c.ContactType))
.GroupBy(c => c.ContactType!)
.ToDictionary(g => g.Key, g => g.Count());
}
// Private mapping methods
private static ContactDTO MapToDTO(Contacts contact)
{
return new ContactDTO
{
Id = contact.Id,
FirstName = contact.FirstName,
LastName = contact.LastName,
Email = contact.Email,
Phone = contact.PhoneNumber,
Mobile = contact.PhoneNumber, // Contacts only has one PhoneNumber field
Title = contact.Title,
Type = contact.ContactType,
AccountId = contact.AccountId,
AccountName = contact.Account?.Name,
CreatedDate = contact.CreatedDate,
CreatedBy = contact.createdby,
ModifiedDate = contact.LastModificationTime,
ModifiedBy = null // Not tracked in FullAuditEntity
};
}
}
}

View file

@ -0,0 +1,160 @@
using Data.SeaHavenIndustries;
using FluentValidation;
using SeaHaven.DataServices.Interfaces;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Interfaces;
using SeaHaven.Services.Validation;
namespace SeaHaven.Services.Implementation
{
/// <summary>
/// Dispatch service implementation
/// Contains business logic, validation, and orchestration for Dispatch operations
/// </summary>
public class DispatchService : IDispatchService
{
private readonly IDispatchDataService _dispatchDataService;
private readonly ICreateDispatchValidation _createValidator;
private readonly IUpdateDispatchValidation _updateValidator;
public DispatchService(
IDispatchDataService dispatchDataService,
ICreateDispatchValidation createValidator,
IUpdateDispatchValidation updateValidator)
{
_dispatchDataService = dispatchDataService;
_createValidator = createValidator;
_updateValidator = updateValidator;
}
public async Task<DispatchDTO?> GetDispatchByIdAsync(int id)
{
var dispatch = await _dispatchDataService.GetByIdAsync(id);
return dispatch == null ? null : MapToDTO(dispatch);
}
public async Task<DispatchDTO?> GetDispatchByIdWithDetailsAsync(int id)
{
var dispatch = await _dispatchDataService.GetByIdWithDetailsAsync(id);
return dispatch == null ? null : MapToDTO(dispatch);
}
public async Task<IEnumerable<DispatchDTO>> GetAllDispatchesAsync()
{
var dispatches = await _dispatchDataService.GetAllAsync();
return dispatches.Select(MapToDTO);
}
public async Task<IEnumerable<DispatchDTO>> GetDispatchesByVendorIdAsync(int vendorId)
{
var dispatches = await _dispatchDataService.GetByVendorIdAsync(vendorId);
return dispatches.Select(MapToDTO);
}
public async Task<PagedResult<DispatchDTO>> GetDispatchesPagedAsync(int page, int pageSize, string? search = null, string? status = null)
{
var (items, totalCount) = await _dispatchDataService.GetPagedAsync(page, pageSize, search, status);
return new PagedResult<DispatchDTO>
{
Items = items.Select(MapToDTO).ToList(),
TotalCount = totalCount,
Page = page,
PageSize = pageSize
};
}
public async Task<DispatchDTO> CreateDispatchAsync(CreateDispatchDTO dto, string userId)
{
var validationResult = await _createValidator.ValidateAsync(dto);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
var dispatchNumber = await GenerateNextDispatchNumberAsync();
var dispatch = new Dispatch
{
VendorId = dto.VendorId,
ScheduledDate = dto.ScheduledDate,
Status = dto.Status ?? "Pending",
DispatchNumber = dispatchNumber,
CreatedDate = DateTime.UtcNow,
createdby = userId
};
var created = await _dispatchDataService.AddAsync(dispatch);
return MapToDTO(created);
}
public async Task<DispatchDTO> UpdateDispatchAsync(int id, UpdateDispatchDTO dto, string userId)
{
var validationResult = await _updateValidator.ValidateAsync(dto);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
var dispatch = await _dispatchDataService.GetByIdAsync(id);
if (dispatch == null)
throw new InvalidOperationException($"Dispatch with ID {id} not found");
if (dto.VendorId.HasValue) dispatch.VendorId = dto.VendorId.Value;
if (dto.ScheduledDate.HasValue) dispatch.ScheduledDate = dto.ScheduledDate;
if (dto.CompletedDate.HasValue) dispatch.CompletedDate = dto.CompletedDate;
if (dto.Status != null) dispatch.Status = dto.Status;
dispatch.LastModificationTime = DateTime.UtcNow;
if (int.TryParse(userId, out int userIdInt))
dispatch.LastModifierUserId = userIdInt;
await _dispatchDataService.UpdateAsync(dispatch);
return MapToDTO(dispatch);
}
public async Task DeleteDispatchAsync(int id, string userId)
{
var dispatch = await _dispatchDataService.GetByIdAsync(id);
if (dispatch == null)
throw new InvalidOperationException($"Dispatch with ID {id} not found");
dispatch.IsDeleted = true;
dispatch.DeleterUserId = userId;
dispatch.DeletionTime = DateTime.UtcNow;
await _dispatchDataService.UpdateAsync(dispatch);
}
public async Task<bool> DispatchExistsAsync(int id)
{
return await _dispatchDataService.ExistsAsync(id);
}
public async Task<string> GenerateNextDispatchNumberAsync()
{
var count = await _dispatchDataService.CountAsync();
return $"DISP-{(count + 1):D6}";
}
public async Task<int> GetTotalDispatchCountAsync()
{
return await _dispatchDataService.CountAsync();
}
private DispatchDTO MapToDTO(Dispatch dispatch)
{
return new DispatchDTO
{
Id = dispatch.Id,
DispatchNumber = dispatch.DispatchNumber,
Status = dispatch.Status,
VendorId = dispatch.VendorId,
VendorName = dispatch.Vendor?.CompanyName,
ScheduledDate = dispatch.ScheduledDate,
CompletedDate = dispatch.CompletedDate,
CreatedDate = dispatch.CreatedDate,
CreatedBy = dispatch.createdby
};
}
}
}

View file

@ -0,0 +1,170 @@
using Data.SeaHavenIndustries;
using FluentValidation;
using SeaHaven.DataServices.Interfaces;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Interfaces;
using SeaHaven.Services.Validation;
namespace SeaHaven.Services.Implementation
{
/// <summary>
/// Employee service implementation
/// Contains business logic, validation, and orchestration for Employee operations
/// </summary>
public class EmployeeService : IEmployeeService
{
private readonly IEmployeeDataService _employeeDataService;
private readonly ICreateEmployeeValidation _createValidator;
private readonly IUpdateEmployeeValidation _updateValidator;
public EmployeeService(
IEmployeeDataService employeeDataService,
ICreateEmployeeValidation createValidator,
IUpdateEmployeeValidation updateValidator)
{
_employeeDataService = employeeDataService;
_createValidator = createValidator;
_updateValidator = updateValidator;
}
// Query operations
public async Task<EmployeeDTO?> GetEmployeeByIdAsync(int id)
{
var employee = await _employeeDataService.GetByIdAsync(id);
return employee == null ? null : MapToDTO(employee);
}
public async Task<EmployeeDTO?> GetEmployeeByIdWithDetailsAsync(int id)
{
var employee = await _employeeDataService.GetByIdWithDetailsAsync(id);
return employee == null ? null : MapToDTO(employee);
}
public async Task<IEnumerable<EmployeeDTO>> GetAllEmployeesAsync()
{
var employees = await _employeeDataService.GetAllAsync();
return employees.Select(MapToDTO);
}
public async Task<PagedResult<EmployeeDTO>> GetEmployeesPagedAsync(int page, int pageSize, string? search = null)
{
var (items, totalCount) = await _employeeDataService.GetPagedAsync(page, pageSize, search);
return new PagedResult<EmployeeDTO>
{
Items = items.Select(MapToDTO).ToList(),
TotalCount = totalCount,
Page = page,
PageSize = pageSize
};
}
public async Task<EmployeeDTO> CreateEmployeeAsync(CreateEmployeeDTO dto, string userId)
{
var validationResult = await _createValidator.ValidateAsync(dto);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
var employee = new Employee
{
FirstName = dto.FirstName,
LastName = dto.LastName,
DepartmentId = dto.DepartmentId,
JobTitleId = dto.JobTitleId,
RegionId = dto.RegionId,
CreatedDate = DateTime.UtcNow,
CreatedBy = userId
};
if (!string.IsNullOrWhiteSpace(dto.Email))
{
employee.Emails.Add(new EmployeeEmail
{
Address = dto.Email,
IsPrimary = true
});
}
if (!string.IsNullOrWhiteSpace(dto.Phone))
{
employee.Phones.Add(new EmployeePhone
{
Number = dto.Phone,
IsPrimary = true
});
}
var created = await _employeeDataService.AddAsync(employee);
return MapToDTO(created);
}
public async Task<EmployeeDTO> UpdateEmployeeAsync(int id, UpdateEmployeeDTO dto, string userId)
{
var validationResult = await _updateValidator.ValidateAsync(dto);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
var employee = await _employeeDataService.GetByIdAsync(id);
if (employee == null)
throw new InvalidOperationException($"Employee with ID {id} not found");
if (dto.FirstName != null) employee.FirstName = dto.FirstName;
if (dto.LastName != null) employee.LastName = dto.LastName;
if (dto.DepartmentId.HasValue) employee.DepartmentId = dto.DepartmentId;
if (dto.JobTitleId.HasValue) employee.JobTitleId = dto.JobTitleId;
if (dto.RegionId.HasValue) employee.RegionId = dto.RegionId;
employee.LastModificationTime = DateTime.UtcNow;
employee.LastModifierUserId = int.Parse(userId);
await _employeeDataService.UpdateAsync(employee);
return MapToDTO(employee);
}
public async Task DeleteEmployeeAsync(int id, string userId)
{
var employee = await _employeeDataService.GetByIdAsync(id);
if (employee == null)
throw new InvalidOperationException($"Employee with ID {id} not found");
employee.IsDeleted = true;
employee.CreatedBy = userId;
employee.LastModificationTime = DateTime.UtcNow;
await _employeeDataService.UpdateAsync(employee);
}
public async Task<bool> EmployeeExistsAsync(int id)
{
return await _employeeDataService.ExistsAsync(id);
}
public async Task<int> GetTotalEmployeeCountAsync()
{
return await _employeeDataService.CountAsync();
}
// Private helper methods
private EmployeeDTO MapToDTO(Employee employee)
{
return new EmployeeDTO
{
Id = employee.Id,
FirstName = employee.FirstName,
LastName = employee.LastName,
Email = employee.Emails?.FirstOrDefault(e => e.IsPrimary == true)?.Address,
Phone = employee.Phones?.FirstOrDefault(p => p.IsPrimary == true)?.Number,
DepartmentId = employee.DepartmentId,
DepartmentName = employee.Department?.Name,
JobTitleId = employee.JobTitleId,
JobTitleName = employee.JobTitle?.Name,
RegionId = employee.RegionId,
RegionName = employee.Region?.Name,
CreatedDate = employee.CreatedDate
};
}
}
}

View file

@ -0,0 +1,171 @@
using Data.SeaHavenIndustries;
using FluentValidation;
using SeaHaven.DataServices.Interfaces;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Interfaces;
using SeaHaven.Services.Validation;
namespace SeaHaven.Services.Implementation
{
/// <summary>
/// Follow-Up service implementation
/// Contains business logic, validation, and orchestration for Follow-Up operations
/// </summary>
public class FollowUpService : IFollowUpService
{
private readonly IFollowUpDataService _followUpDataService;
private readonly ICreateFollowUpValidation _createValidator;
private readonly IUpdateFollowUpValidation _updateValidator;
public FollowUpService(
IFollowUpDataService followUpDataService,
ICreateFollowUpValidation createValidator,
IUpdateFollowUpValidation updateValidator)
{
_followUpDataService = followUpDataService;
_createValidator = createValidator;
_updateValidator = updateValidator;
}
public async Task<FollowUpDTO?> GetFollowUpByIdAsync(int id)
{
var followUp = await _followUpDataService.GetByIdAsync(id);
return followUp == null ? null : MapToDTO(followUp);
}
public async Task<FollowUpDTO?> GetFollowUpByIdWithDetailsAsync(int id)
{
var followUp = await _followUpDataService.GetByIdWithDetailsAsync(id);
return followUp == null ? null : MapToDTO(followUp);
}
public async Task<IEnumerable<FollowUpDTO>> GetAllFollowUpsAsync()
{
var followUps = await _followUpDataService.GetAllAsync();
return followUps.Select(MapToDTO);
}
public async Task<IEnumerable<FollowUpDTO>> GetFollowUpsByWorkOrderIdAsync(int workOrderId)
{
var followUps = await _followUpDataService.GetByWorkOrderIdAsync(workOrderId);
return followUps.Select(MapToDTO);
}
public async Task<PagedResult<FollowUpDTO>> GetFollowUpsPagedAsync(int page, int pageSize, string? search = null, string? status = null)
{
var (items, totalCount) = await _followUpDataService.GetPagedAsync(page, pageSize, search);
var filteredItems = items.AsEnumerable();
if (!string.IsNullOrWhiteSpace(status))
{
filteredItems = filteredItems.Where(f => f.Status == status);
}
return new PagedResult<FollowUpDTO>
{
Items = filteredItems.Select(MapToDTO).ToList(),
TotalCount = totalCount,
Page = page,
PageSize = pageSize
};
}
public async Task<FollowUpDTO> CreateFollowUpAsync(CreateFollowUpDTO dto, string userId)
{
var validationResult = await _createValidator.ValidateAsync(dto);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
var followUp = new FollowUps
{
Reason = dto.Reason,
ScheduleDate = dto.ScheduleDate,
ScheduleStartTime = dto.ScheduleStartTime,
Status = dto.Status,
EmployeeId = dto.EmployeeId,
WorkOrderId = dto.WorkOrderId,
AccountId = dto.AccountId,
LocationId = dto.LocationId,
CreatedDate = DateTime.UtcNow,
createdby = userId
};
var created = await _followUpDataService.AddAsync(followUp);
return MapToDTO(created);
}
public async Task<FollowUpDTO> UpdateFollowUpAsync(int id, UpdateFollowUpDTO dto, string userId)
{
var validationResult = await _updateValidator.ValidateAsync(dto);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
var followUp = await _followUpDataService.GetByIdAsync(id);
if (followUp == null)
throw new InvalidOperationException($"Follow-Up with ID {id} not found");
if (dto.Reason != null) followUp.Reason = dto.Reason;
if (dto.ScheduleDate.HasValue) followUp.ScheduleDate = dto.ScheduleDate.Value;
if (dto.ScheduleStartTime.HasValue) followUp.ScheduleStartTime = dto.ScheduleStartTime.Value;
if (dto.Status != null) followUp.Status = dto.Status;
if (dto.EmployeeId.HasValue) followUp.EmployeeId = dto.EmployeeId;
if (dto.WorkOrderId.HasValue) followUp.WorkOrderId = dto.WorkOrderId;
if (dto.AccountId.HasValue) followUp.AccountId = dto.AccountId;
if (dto.LocationId.HasValue) followUp.LocationId = dto.LocationId;
followUp.LastModificationTime = DateTime.UtcNow;
if (int.TryParse(userId, out int userIdInt))
followUp.LastModifierUserId = userIdInt;
await _followUpDataService.UpdateAsync(followUp);
return MapToDTO(followUp);
}
public async Task DeleteFollowUpAsync(int id, string userId)
{
var followUp = await _followUpDataService.GetByIdAsync(id);
if (followUp == null)
throw new InvalidOperationException($"Follow-Up with ID {id} not found");
followUp.IsDeleted = true;
followUp.DeleterUserId = userId;
followUp.DeletionTime = DateTime.UtcNow;
await _followUpDataService.UpdateAsync(followUp);
}
public async Task<bool> FollowUpExistsAsync(int id)
{
return await _followUpDataService.ExistsAsync(id);
}
public async Task<int> GetTotalFollowUpCountAsync()
{
return await _followUpDataService.CountAsync();
}
private FollowUpDTO MapToDTO(FollowUps followUp)
{
return new FollowUpDTO
{
Id = followUp.Id,
Reason = followUp.Reason,
ScheduleDate = followUp.ScheduleDate,
ScheduleStartTime = followUp.ScheduleStartTime,
Status = followUp.Status,
EmployeeId = followUp.EmployeeId,
EmployeeName = followUp.Employee != null ? $"{followUp.Employee.FirstName} {followUp.Employee.LastName}" : null,
WorkOrderId = followUp.WorkOrderId,
AccountId = followUp.AccountId,
AccountName = followUp.Account?.Name,
LocationId = followUp.LocationId,
LocationName = followUp.Location?.Name,
CreatedDate = followUp.CreatedDate
};
}
}
}

View file

@ -0,0 +1,163 @@
using Data.SeaHavenIndustries;
using FluentValidation;
using SeaHaven.DataServices.Interfaces;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Interfaces;
using SeaHaven.Services.Validation;
namespace SeaHaven.Services.Implementation
{
/// <summary>
/// Location service implementation
/// Contains business logic, validation, and orchestration for Location operations
/// </summary>
public class LocationService : ILocationService
{
private readonly ILocationDataService _locationDataService;
private readonly ICreateLocationValidation _createValidator;
private readonly IUpdateLocationValidation _updateValidator;
public LocationService(
ILocationDataService locationDataService,
ICreateLocationValidation createValidator,
IUpdateLocationValidation updateValidator)
{
_locationDataService = locationDataService;
_createValidator = createValidator;
_updateValidator = updateValidator;
}
// Query operations
public async Task<LocationDTO?> GetLocationByIdAsync(int id)
{
var location = await _locationDataService.GetByIdAsync(id);
return location == null ? null : MapToDTO(location);
}
public async Task<LocationDTO?> GetLocationByIdWithDetailsAsync(int id)
{
var location = await _locationDataService.GetByIdWithDetailsAsync(id);
return location == null ? null : MapToDTO(location);
}
public async Task<IEnumerable<LocationDTO>> GetAllLocationsAsync()
{
var locations = await _locationDataService.GetAllAsync();
return locations.Select(MapToDTO);
}
public async Task<IEnumerable<LocationDTO>> GetLocationsByAccountIdAsync(int accountId)
{
var locations = await _locationDataService.GetByAccountIdAsync(accountId);
return locations.Select(MapToDTO);
}
public async Task<PagedResult<LocationDTO>> GetLocationsPagedAsync(int page, int pageSize, string? search = null)
{
var (items, totalCount) = await _locationDataService.GetPagedAsync(page, pageSize, search);
return new PagedResult<LocationDTO>
{
Items = items.Select(MapToDTO).ToList(),
TotalCount = totalCount,
Page = page,
PageSize = pageSize
};
}
public async Task<LocationDTO> CreateLocationAsync(CreateLocationDTO dto, string userId)
{
var validationResult = await _createValidator.ValidateAsync(dto);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
var location = new Locations
{
Name = dto.LocationName,
Title = dto.LocationName,
Address1 = dto.Address,
City = dto.City,
State = dto.State,
Zip = dto.Zipcode,
CreatedDate = DateTime.UtcNow,
createdby = userId
};
var created = await _locationDataService.AddAsync(location);
return MapToDTO(created);
}
public async Task<LocationDTO> UpdateLocationAsync(int id, UpdateLocationDTO dto, string userId)
{
var validationResult = await _updateValidator.ValidateAsync(dto);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
var location = await _locationDataService.GetByIdAsync(id);
if (location == null)
throw new InvalidOperationException($"Location with ID {id} not found");
// Update properties
if (dto.LocationName != null) location.Name = dto.LocationName;
if (dto.LocationName != null) location.Title = dto.LocationName;
if (dto.Address != null) location.Address1 = dto.Address;
if (dto.City != null) location.City = dto.City;
if (dto.State != null) location.State = dto.State;
if (dto.Zipcode != null) location.Zip = dto.Zipcode;
location.LastModificationTime = DateTime.UtcNow;
if (int.TryParse(userId, out int userIdInt))
location.LastModifierUserId = userIdInt;
await _locationDataService.UpdateAsync(location);
return MapToDTO(location);
}
public async Task DeleteLocationAsync(int id, string userId)
{
var location = await _locationDataService.GetByIdAsync(id);
if (location == null)
throw new InvalidOperationException($"Location with ID {id} not found");
location.IsDeleted = true;
location.DeleterUserId = userId;
location.DeletionTime = DateTime.UtcNow;
await _locationDataService.UpdateAsync(location);
}
public async Task<bool> LocationExistsAsync(int id)
{
return await _locationDataService.ExistsAsync(id);
}
public async Task<int> GetTotalLocationCountAsync()
{
return await _locationDataService.CountAsync();
}
// Private helper methods
private LocationDTO MapToDTO(Locations location)
{
return new LocationDTO
{
Id = location.Id,
Name = location.Name,
Title = location.Title,
Address1 = location.Address1,
Address2 = location.Address2,
City = location.City,
State = location.State,
Zip = location.Zip,
PhoneNumber = location.PhoneNumber,
Email = location.Email,
Status = location.Status,
CreatedDate = location.CreatedDate,
CreatedBy = location.createdby
};
}
}
}

View file

@ -0,0 +1,157 @@
using Data.SeaHavenIndustries;
using FluentValidation;
using SeaHaven.DataServices.Interfaces;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Interfaces;
using SeaHaven.Services.Validation;
namespace SeaHaven.Services.Implementation
{
/// <summary>
/// PM Schedule service implementation
/// Contains business logic, validation, and orchestration for PM Schedule operations
/// </summary>
public class PMScheduleService : IPMScheduleService
{
private readonly IPMScheduleDataService _pmScheduleDataService;
private readonly ICreatePMScheduleValidation _createValidator;
private readonly IUpdatePMScheduleValidation _updateValidator;
public PMScheduleService(
IPMScheduleDataService pmScheduleDataService,
ICreatePMScheduleValidation createValidator,
IUpdatePMScheduleValidation updateValidator)
{
_pmScheduleDataService = pmScheduleDataService;
_createValidator = createValidator;
_updateValidator = updateValidator;
}
public async Task<PMScheduleDTO?> GetPMScheduleByIdAsync(int id)
{
var pmSchedule = await _pmScheduleDataService.GetByIdAsync(id);
return pmSchedule == null ? null : MapToDTO(pmSchedule);
}
public async Task<PMScheduleDTO?> GetPMScheduleByIdWithDetailsAsync(int id)
{
var pmSchedule = await _pmScheduleDataService.GetByIdWithDetailsAsync(id);
return pmSchedule == null ? null : MapToDTO(pmSchedule);
}
public async Task<IEnumerable<PMScheduleDTO>> GetAllPMSchedulesAsync()
{
var pmSchedules = await _pmScheduleDataService.GetAllWithDetailsAsync();
return pmSchedules.Select(MapToDTO);
}
public async Task<IEnumerable<PMScheduleDTO>> GetPMSchedulesByLocationIdAsync(int locationId)
{
var pmSchedules = await _pmScheduleDataService.GetAllWithDetailsAsync();
return pmSchedules.Where(p => p.LocationId == locationId).Select(MapToDTO);
}
public async Task<PagedResult<PMScheduleDTO>> GetPMSchedulesPagedAsync(int page, int pageSize, string? search = null)
{
var (items, totalCount) = await _pmScheduleDataService.GetPagedAsync(page, pageSize, search);
return new PagedResult<PMScheduleDTO>
{
Items = items.Select(MapToDTO).ToList(),
TotalCount = totalCount,
Page = page,
PageSize = pageSize
};
}
public async Task<PMScheduleDTO> CreatePMScheduleAsync(CreatePMScheduleDTO dto, string userId)
{
var validationResult = await _createValidator.ValidateAsync(dto);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
var pmSchedule = new PMSchedules
{
Name = dto.Name,
Frequency = dto.Frequency,
StartDate = dto.StartDate,
IsActive = dto.IsActive,
AssetId = dto.AssetId,
LocationId = dto.LocationId,
CreatedDate = DateTime.UtcNow,
createdby = userId
};
var created = await _pmScheduleDataService.AddAsync(pmSchedule);
return MapToDTO(created);
}
public async Task<PMScheduleDTO> UpdatePMScheduleAsync(int id, UpdatePMScheduleDTO dto, string userId)
{
var validationResult = await _updateValidator.ValidateAsync(dto);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
var pmSchedule = await _pmScheduleDataService.GetByIdAsync(id);
if (pmSchedule == null)
throw new InvalidOperationException($"PM Schedule with ID {id} not found");
if (dto.Name != null) pmSchedule.Name = dto.Name;
if (dto.Frequency != null) pmSchedule.Frequency = dto.Frequency;
if (dto.StartDate.HasValue) pmSchedule.StartDate = dto.StartDate.Value;
if (dto.IsActive.HasValue) pmSchedule.IsActive = dto.IsActive.Value;
if (dto.AssetId.HasValue) pmSchedule.AssetId = dto.AssetId;
if (dto.LocationId.HasValue) pmSchedule.LocationId = dto.LocationId;
pmSchedule.LastModificationTime = DateTime.UtcNow;
if (int.TryParse(userId, out int userIdInt))
pmSchedule.LastModifierUserId = userIdInt;
await _pmScheduleDataService.UpdateAsync(pmSchedule);
return MapToDTO(pmSchedule);
}
public async Task DeletePMScheduleAsync(int id, string userId)
{
var pmSchedule = await _pmScheduleDataService.GetByIdAsync(id);
if (pmSchedule == null)
throw new InvalidOperationException($"PM Schedule with ID {id} not found");
pmSchedule.IsDeleted = true;
pmSchedule.DeleterUserId = userId;
pmSchedule.DeletionTime = DateTime.UtcNow;
await _pmScheduleDataService.UpdateAsync(pmSchedule);
}
public async Task<bool> PMScheduleExistsAsync(int id)
{
return await _pmScheduleDataService.ExistsAsync(id);
}
public async Task<int> GetTotalPMScheduleCountAsync()
{
return await _pmScheduleDataService.CountAsync();
}
private PMScheduleDTO MapToDTO(PMSchedules pmSchedule)
{
return new PMScheduleDTO
{
Id = pmSchedule.Id,
Name = pmSchedule.Name,
Frequency = pmSchedule.Frequency,
StartDate = pmSchedule.StartDate,
IsActive = pmSchedule.IsActive,
AssetId = pmSchedule.AssetId,
AssetName = pmSchedule.Asset?.Name,
LocationId = pmSchedule.LocationId,
LocationName = pmSchedule.Location?.Name,
CreatedDate = pmSchedule.CreatedDate
};
}
}
}

View file

@ -0,0 +1,151 @@
using Data.SeaHavenIndustries;
using FluentValidation;
using SeaHaven.DataServices.Interfaces;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Interfaces;
using SeaHaven.Services.Validation;
namespace SeaHaven.Services.Implementation
{
/// <summary>
/// Vendor service implementation
/// Contains business logic, validation, and orchestration for Vendor operations
/// </summary>
public class VendorService : IVendorService
{
private readonly IVendorDataService _vendorDataService;
private readonly ICreateVendorValidation _createValidator;
private readonly IUpdateVendorValidation _updateValidator;
public VendorService(
IVendorDataService vendorDataService,
ICreateVendorValidation createValidator,
IUpdateVendorValidation updateValidator)
{
_vendorDataService = vendorDataService;
_createValidator = createValidator;
_updateValidator = updateValidator;
}
public async Task<VendorDTO?> GetVendorByIdAsync(int id)
{
var vendor = await _vendorDataService.GetByIdAsync(id);
return vendor == null ? null : MapToDTO(vendor);
}
public async Task<VendorDTO?> GetVendorByIdWithDetailsAsync(int id)
{
var vendor = await _vendorDataService.GetByIdWithDetailsAsync(id);
return vendor == null ? null : MapToDTO(vendor);
}
public async Task<IEnumerable<VendorDTO>> GetAllVendorsAsync()
{
var vendors = await _vendorDataService.GetAllAsync();
return vendors.Select(MapToDTO);
}
public async Task<PagedResult<VendorDTO>> GetVendorsPagedAsync(int page, int pageSize, string? search = null)
{
var (items, totalCount) = await _vendorDataService.GetPagedAsync(page, pageSize, search);
return new PagedResult<VendorDTO>
{
Items = items.Select(MapToDTO).ToList(),
TotalCount = totalCount,
Page = page,
PageSize = pageSize
};
}
public async Task<VendorDTO> CreateVendorAsync(CreateVendorDTO dto, string userId)
{
var validationResult = await _createValidator.ValidateAsync(dto);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
var vendor = new Vendor
{
CompanyName = dto.Name,
Email = dto.Email,
Phone = dto.Phone,
Address = dto.Address,
City = dto.City,
State = dto.State,
Zip = dto.Zipcode,
CreatedDate = DateTime.UtcNow,
createdby = userId
};
var created = await _vendorDataService.AddAsync(vendor);
return MapToDTO(created);
}
public async Task<VendorDTO> UpdateVendorAsync(int id, UpdateVendorDTO dto, string userId)
{
var validationResult = await _updateValidator.ValidateAsync(dto);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
var vendor = await _vendorDataService.GetByIdAsync(id);
if (vendor == null)
throw new InvalidOperationException($"Vendor with ID {id} not found");
if (dto.Name != null) vendor.CompanyName = dto.Name;
if (dto.Email != null) vendor.Email = dto.Email;
if (dto.Phone != null) vendor.Phone = dto.Phone;
if (dto.Address != null) vendor.Address = dto.Address;
if (dto.City != null) vendor.City = dto.City;
if (dto.State != null) vendor.State = dto.State;
if (dto.Zipcode != null) vendor.Zip = dto.Zipcode;
vendor.LastModificationTime = DateTime.UtcNow;
if (int.TryParse(userId, out int userIdInt))
vendor.LastModifierUserId = userIdInt;
await _vendorDataService.UpdateAsync(vendor);
return MapToDTO(vendor);
}
public async Task DeleteVendorAsync(int id, string userId)
{
var vendor = await _vendorDataService.GetByIdAsync(id);
if (vendor == null)
throw new InvalidOperationException($"Vendor with ID {id} not found");
vendor.IsDeleted = true;
vendor.DeleterUserId = userId;
vendor.DeletionTime = DateTime.UtcNow;
await _vendorDataService.UpdateAsync(vendor);
}
public async Task<bool> VendorExistsAsync(int id)
{
return await _vendorDataService.ExistsAsync(id);
}
public async Task<int> GetTotalVendorCountAsync()
{
return await _vendorDataService.CountAsync();
}
private VendorDTO MapToDTO(Vendor vendor)
{
return new VendorDTO
{
Id = vendor.Id,
Name = vendor.CompanyName,
Email = vendor.Email,
Phone = vendor.Phone,
Address = vendor.Address,
City = vendor.City,
State = vendor.State,
Zipcode = vendor.Zip
};
}
}
}

View file

@ -0,0 +1,168 @@
using Data.SeaHavenIndustries;
using Data.SeaHavenIndustries.Enums;
using FluentValidation;
using SeaHaven.DataServices.Interfaces;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Interfaces;
using SeaHaven.Services.Validation;
namespace SeaHaven.Services.Implementation
{
public class WorkOrderService : IWorkOrderService
{
private readonly IWorkOrderDataService _workOrderDataService;
private readonly ICreateWorkOrderValidation _createValidator;
private readonly IUpdateWorkOrderValidation _updateValidator;
public WorkOrderService(
IWorkOrderDataService workOrderDataService,
ICreateWorkOrderValidation createValidator,
IUpdateWorkOrderValidation updateValidator)
{
_workOrderDataService = workOrderDataService;
_createValidator = createValidator;
_updateValidator = updateValidator;
}
public async Task<WorkOrderDTO?> GetWorkOrderByIdAsync(int id)
{
var workOrder = await _workOrderDataService.GetByIdAsync(id);
return workOrder == null ? null : WorkOrderDTO.MapToModel(workOrder);
}
public async Task<WorkOrderDTO?> GetWorkOrderByIdWithDetailsAsync(int id)
{
var workOrder = await _workOrderDataService.GetByIdWithDetailsAsync(id);
return workOrder == null ? null : WorkOrderDTO.MapToModel(workOrder);
}
public async Task<IEnumerable<WorkOrderDTO>> GetAllWorkOrdersAsync()
{
var workOrders = await _workOrderDataService.GetAllAsync();
return workOrders.Select(WorkOrderDTO.MapToModel);
}
public async Task<PagedResult<WorkOrderDTO>> GetWorkOrdersPagedAsync(int page, int pageSize, string? search = null, string? status = null, string? priority = null)
{
var (items, totalCount) = await _workOrderDataService.GetPagedAsync(page, pageSize, search, status, priority);
return new PagedResult<WorkOrderDTO>
{
Items = items.Select(WorkOrderDTO.MapToModel),
TotalCount = totalCount,
Page = page,
PageSize = pageSize
};
}
public async Task<int> CreateWorkOrderAsync(CreateWorkOrderDTO dto, string userId)
{
var validationResult = await _createValidator.ValidateAsync(dto);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
var workOrder = CreateWorkOrderDTO.MapToEntity(dto);
workOrder.createdby = userId;
workOrder.CreatedDate = DateTime.UtcNow;
workOrder.InternalWONumber = await _workOrderDataService.GenerateNextInternalWONumberAsync();
var saved = await _workOrderDataService.AddAsync(workOrder);
await LogAuditAsync(saved.Id, userId, AuditAction.Create, "Work order created");
return saved.Id;
}
public async Task<bool> UpdateWorkOrderAsync(int id, UpdateWorkOrderDTO dto, string userId)
{
var validationResult = await _updateValidator.ValidateAsync(dto);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
var existingWorkOrder = await _workOrderDataService.GetByIdAsync(id);
if (existingWorkOrder == null)
return false;
var changes = new List<string>();
changes.AddRange(TrackChanges(existingWorkOrder, dto));
UpdateWorkOrderDTO.UpdateEntity(dto, existingWorkOrder);
await _workOrderDataService.UpdateAsync(existingWorkOrder);
if (changes.Any())
{
await LogAuditAsync(id, userId, AuditAction.Edit, string.Join(", ", changes));
}
return true;
}
public async Task<bool> DeleteWorkOrderAsync(int id, string userId)
{
if (!await _workOrderDataService.ExistsAsync(id))
return false;
await _workOrderDataService.DeleteWorkOrderAttachmentsAsync(id);
await _workOrderDataService.DeleteWorkOrderContactsAsync(id);
await _workOrderDataService.DeleteWorkOrderCategoriesAsync(id);
await _workOrderDataService.DeleteAsync(id);
await LogAuditAsync(id, userId, AuditAction.Delete, "Work order deleted");
return true;
}
public async Task<bool> WorkOrderExistsAsync(int id)
{
return await _workOrderDataService.ExistsAsync(id);
}
public async Task<string> GenerateNextInternalWONumberAsync()
{
return await _workOrderDataService.GenerateNextInternalWONumberAsync();
}
public async Task<int> GetTotalWorkOrderCountAsync()
{
return await _workOrderDataService.CountAsync();
}
private List<string> TrackChanges(WorkOrder existing, UpdateWorkOrderDTO dto)
{
var changes = new List<string>();
if (dto.WorkerOrderTitle != null && dto.WorkerOrderTitle != existing.WorkerOrderTitle)
changes.Add($"Title: {existing.WorkerOrderTitle} → {dto.WorkerOrderTitle}");
if (dto.Status.HasValue && dto.Status.ToString() != existing.Status)
changes.Add($"Status: {existing.Status} → {dto.Status}");
if (dto.Priority.HasValue && dto.Priority.ToString() != existing.Priority)
changes.Add($"Priority: {existing.Priority} → {dto.Priority}");
if (dto.AssignTo != null && dto.AssignTo != existing.AssignTo)
changes.Add($"Assigned To: {existing.AssignTo} → {dto.AssignTo}");
if (dto.DueDate.HasValue && dto.DueDate != existing.DueDate)
changes.Add($"Due Date: {existing.DueDate:yyyy-MM-dd} → {dto.DueDate:yyyy-MM-dd}");
return changes;
}
private async Task LogAuditAsync(int workOrderId, string userId, AuditAction action, string description)
{
var auditLog = new WorkOrderAuditLog
{
WorkOrderId = workOrderId,
Action = action.ToString(),
FieldName = description,
UserId = userId,
CreatedAt = DateTime.UtcNow
};
await _workOrderDataService.AddAuditLogAsync(auditLog);
}
}
}

View file

@ -0,0 +1,17 @@
using SeaHaven.Services.DTOs;
namespace SeaHaven.Services.Interfaces
{
public interface IAccountService
{
Task<AccountDTO?> GetAccountByIdAsync(int id);
Task<AccountDTO?> GetAccountByIdWithDetailsAsync(int id);
Task<IEnumerable<AccountDTO>> GetAllAccountsAsync();
Task<PagedResult<AccountDTO>> GetAccountsPagedAsync(int page, int pageSize, string? search = null);
Task<AccountDTO> CreateAccountAsync(CreateAccountDTO dto, string userId);
Task<AccountDTO> UpdateAccountAsync(int id, UpdateAccountDTO dto, string userId);
Task DeleteAccountAsync(int id, string userId);
Task<bool> AccountExistsAsync(int id);
Task<int> GetTotalAccountCountAsync();
}
}

Some files were not shown because too many files have changed in this diff Show more