Merge pull request #2 from Sea-Haven-Industries/Dev

Dev
This commit is contained in:
Adam Moussa 2026-05-14 17:35:05 -04:00 • committed by GitHub
commit 50a4bf57f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 3038 additions and 1393 deletions

View file

@ -1,9 +1,11 @@
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")]
@ -33,7 +35,10 @@ public class AccountController : ControllerBase
Website = dto.Website
};
var userId = User.Identity?.Name ?? "system";
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
@ -42,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 });
}
}
@ -68,7 +74,10 @@ public class AccountController : ControllerBase
Website = dto.Website
};
var userId = User.Identity?.Name ?? "system";
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
await _accountService.UpdateAccountAsync(dto.Id, updateDto, userId);
return Ok(new DataResponse
@ -77,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 });
}
}
@ -95,7 +109,10 @@ public class AccountController : ControllerBase
{
try
{
var userId = User.Identity?.Name ?? "system";
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
await _accountService.DeleteAccountAsync(id, userId);
return Ok(new DataResponse
@ -104,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 });
}
}

View file

@ -1,11 +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
{
@ -35,14 +37,22 @@ namespace Api.SeaHavenIndustries.Controllers
LocationId = asset.LocationId
};
var userId = User.Identity?.Name ?? "system";
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 });
}
}
@ -58,14 +68,26 @@ namespace Api.SeaHavenIndustries.Controllers
LocationId = asset.LocationId
};
var userId = User.Identity?.Name ?? "system";
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);
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 });
}
}
@ -75,13 +97,20 @@ namespace Api.SeaHavenIndustries.Controllers
{
try
{
var userId = User.Identity?.Name ?? "system";
await _assetService.DeleteAssetAsync(id, userId);
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 });
}
}

View file

@ -1,5 +1,6 @@
using Api.SeaHavenIndustries.DTOs;
using Data.SeaHavenIndustries;
using FluentValidation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
@ -34,15 +35,22 @@ namespace Api.SeaHavenIndustries.Controllers
{
try
{
// ✅ Use DTO mapping method instead of manual property assignment
var userId = _userManager.GetUserId(User) ?? "system";
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 = "Updated Successfully", Status = "200" });
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 });
}
}
@ -52,19 +60,26 @@ namespace Api.SeaHavenIndustries.Controllers
{
try
{
// ✅ Use DTO mapping method instead of manual property assignment
var userId = _userManager.GetUserId(User) ?? "system";
var userId = _userManager.GetUserId(User);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
await _contactService.UpdateContactAsync(contacts.Id, contacts.ToServiceUpdateDTO(), userId);
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
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 BadRequest(new Response { Status = "Error", Message = "Contact not found" });
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 });
}
}
@ -74,30 +89,28 @@ namespace Api.SeaHavenIndustries.Controllers
{
try
{
var userId = _userManager.GetUserId(User) ?? "system";
var userId = _userManager.GetUserId(User);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
// ✅ Call Service layer (not DataService directly)
await _contactService.DeleteContactAsync(id, userId);
return Ok(new DataResponse { Message = "Removed Successfully", Status = "200" });
return Ok(new DataResponse { Message = "Contact deleted successfully", Status = "200" });
}
catch (KeyNotFoundException)
{
return BadRequest(new Response { Status = "Error", Message = "ID not matched!" });
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 async Task<IActionResult> GetContactList(string? search ="", int page = 1, int pageSize = 10)
{
// ✅ Call Service layer
var pagedResult = await _contactService.GetContactsPagedAsync(page, pageSize, search);
// ✅ Use DTO mapping method (NO manual transformation in controller)
var data = ContactListItem_DTO.FromServiceDTOs(pagedResult.Items);
var totalPages = (int)Math.Ceiling(pagedResult.TotalCount / (double)pageSize);
@ -116,7 +129,6 @@ namespace Api.SeaHavenIndustries.Controllers
[Route("GetContacts")]
public async Task<IActionResult> GetContacts()
{
// ✅ Call Service layer
var data = await _contactService.GetAllContactsAsync();
return Ok(data);
}
@ -124,24 +136,15 @@ namespace Api.SeaHavenIndustries.Controllers
[Route("GetLocations")]
public async Task<IActionResult> GetLocations()
{
// ✅ Call Service layer
var locations = await _locationService.GetAllLocationsAsync();
// ✅ Use DTO mapping method (NO manual transformation in controller)
var data = LocationViewModel_DTO.FromServiceDTOs(locations);
return Ok(data);
}
// TODO: ARCHITECTURAL VIOLATION - These methods should NOT be in ContactController
// GetCategories should be in a CategoryController with ICategoryService
// GetUsers should be in a UserController with IUserService
// For now, commenting out to prevent direct database access
[HttpGet]
[Route("GetCategories")]
public async Task<IActionResult> GetCategories()
{
// ❌ TEMPORARY: Needs ICategoryService
return StatusCode(501, new Response { Status = "Error", Message = "Not Implemented - Needs CategoryService" });
}
@ -149,11 +152,8 @@ namespace Api.SeaHavenIndustries.Controllers
[Route("GetUsers")]
public async Task<IActionResult> GetUsers()
{
// ❌ TEMPORARY: Needs IUserService
return StatusCode(501, new Response { Status = "Error", Message = "Not Implemented - Needs UserService" });
}
// TODO: ARCHITECTURAL VIOLATION - Location CRUD methods should NOT be in ContactController
// These should be in LocationController with ILocationService
[HttpPost]
[Route("AddLocation")]
@ -161,7 +161,6 @@ namespace Api.SeaHavenIndustries.Controllers
{
try
{
// ✅ Use Service layer with DTO mapping
var userId = _userManager.GetUserId(User) ?? "system";
await _locationService.CreateLocationAsync(model.ToServiceCreateDTO(), userId);
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
@ -178,7 +177,6 @@ namespace Api.SeaHavenIndustries.Controllers
{
try
{
// ✅ Use Service layer with DTO mapping
var userId = _userManager.GetUserId(User) ?? "system";
await _locationService.UpdateLocationAsync(model.Id, model.ToServiceUpdateDTO(), userId);
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
@ -199,7 +197,6 @@ namespace Api.SeaHavenIndustries.Controllers
{
try
{
// ✅ Use Service layer
var userId = _userManager.GetUserId(User) ?? "system";
await _locationService.DeleteLocationAsync(Id, userId);
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
@ -218,10 +215,7 @@ namespace Api.SeaHavenIndustries.Controllers
[Route("GetAddressbook")]
public async Task<IActionResult> GetAddressbook(string? search = "", int page = 1, int pageSize = 10)
{
// ✅ Call Service layer
var pagedResult = await _locationService.GetLocationsPagedAsync(page, pageSize, search);
// ✅ Use DTO mapping method (NO manual transformation in controller)
var data = LocationListItem_DTO.FromServiceDTOs(pagedResult.Items);
var totalPages = (int)Math.Ceiling(pagedResult.TotalCount / (double)pageSize);

View file

@ -1,9 +1,11 @@
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")]
@ -18,9 +20,10 @@ public class EmployeeController : Controller
}
// -----------------------------------
// POST: api/Employee/AddEmployees
// POST: api/Employee/AddEmployees or /Create
// -----------------------------------
[HttpPost("AddEmployees")]
[HttpPost("Create")]
public async Task<IActionResult> AddEmployees(AddEmployee_DTO dto)
{
try
@ -35,21 +38,30 @@ public class EmployeeController : Controller
Phone = dto.Phone
};
var userId = User.Identity?.Name ?? "system";
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)
{
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 { Message = ex.Message });
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
// -----------------------------------
// POST: api/Employee/EditEmployees
// POST: api/Employee/EditEmployees or /Update
// -----------------------------------
[HttpPost("EditEmployees")]
[HttpPost("Update")]
public async Task<IActionResult> EditEmployees(EditEmployee_DTO dto)
{
try
@ -61,14 +73,26 @@ public class EmployeeController : Controller
JobTitleId = dto.JobTitleId
};
var userId = User.Identity?.Name ?? "system";
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
await _employeeService.UpdateEmployeeAsync(dto.Id, updateDto, userId);
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 BadRequest(new { Message = ex.Message });
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
@ -106,14 +130,50 @@ public class EmployeeController : Controller
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 userId = User.Identity?.Name ?? "system";
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
await _employeeService.DeleteEmployeeAsync(id, userId);
return Ok(new DataResponse
@ -130,14 +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 BadRequest(new Response
{
Status = "Error",
Message = ex.Message
});
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,6 +1,7 @@
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;
@ -157,44 +158,75 @@ 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" });
await _followUpDataService.AddAsync(model);
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 _followUpDataService.GetByIdAsync(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 _followUpDataService.UpdateAsync(model);
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")]
@ -226,11 +258,23 @@ public class FollowUpController : ControllerBase
[HttpPost("Delete")]
public async Task<IActionResult> Delete(int id)
{
var model = await _followUpDataService.GetByIdAsync(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" });
await _followUpDataService.DeleteAsync(id);
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,11 +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
{
@ -108,9 +110,14 @@ namespace Api.SeaHavenIndustries.Controllers
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 });
}
}
@ -121,14 +128,14 @@ namespace Api.SeaHavenIndustries.Controllers
{
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.Address1 = model.Address;
exist.City = model.City;
exist.State = model.State;
exist.Zip = model.ZipCode; // This is a property, not a method
exist.Zip = model.ZipCode;
exist.PhoneNumber = model.Phone;
exist.Email = model.ContactEmail;
exist.Status = model.Status;
@ -137,9 +144,18 @@ namespace Api.SeaHavenIndustries.Controllers
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 });
}
}
@ -150,14 +166,18 @@ namespace Api.SeaHavenIndustries.Controllers
{
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" });
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,9 +1,11 @@
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")]
@ -36,12 +38,46 @@ public class PMScheduleController : ControllerBase
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)
{
try
{
var userId = User.Identity?.Name ?? "system";
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
if (dto.Id == 0)
{
@ -72,9 +108,40 @@ public class PMScheduleController : ControllerBase
return Ok();
}
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 = "PM Schedule not found" });
}
catch (Exception ex)
{
return BadRequest(new { Message = ex.Message });
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
[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 _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

@ -1,10 +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
{
@ -53,9 +55,13 @@ namespace Api.SeaHavenIndustries.Controllers
}
[HttpGet("{id}")]
public async Task<IActionResult> GetVendorById(int id)
[HttpGet("GetById")]
public async Task<IActionResult> GetVendorById([FromQuery] int? id)
{
var vendor = await _vendorService.GetVendorByIdAsync(id);
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" });
@ -64,6 +70,7 @@ namespace Api.SeaHavenIndustries.Controllers
}
[HttpPost]
[HttpPost("Create")]
public async Task<IActionResult> Create([FromBody] Vendor_DTO model)
{
try
@ -73,42 +80,88 @@ namespace Api.SeaHavenIndustries.Controllers
Name = model.CompanyName ?? throw new ArgumentException("CompanyName is required")
};
var userId = User.Identity?.Name ?? "system";
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
{
// 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" });
var updateDto = new UpdateVendorDTO
{
Name = model.CompanyName
};
var userId = User.Identity?.Name ?? "system";
await _vendorService.UpdateVendorAsync(id, updateDto, userId);
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 userId = User.Identity?.Name ?? "system";
await _vendorService.DeleteVendorAsync(id, userId);
return Ok(new DataResponse { Message = "Vendor Deleted", Status = "200" });
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" });
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")]

File diff suppressed because it is too large Load diff

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

@ -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,7 +33,7 @@ builder.Services.AddResponseCompression(opts =>
});
builder.Services.AddCors(option =>
option.AddDefaultPolicy(builder => builder.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod()));
// Helper services
builder.Services.AddScoped<SendMessage>();
builder.Services.AddScoped<Api.SeaHavenIndustries.Helper.VendorPortalTokenService>();
builder.Services.AddSingleton<Api.SeaHavenIndustries.Helper.ZipCodeDistance>(sp =>
@ -43,36 +45,8 @@ builder.Services.AddSingleton<Api.SeaHavenIndustries.Helper.ZipCodeDistance>(sp
return zipService;
});
// ========================================================================
// MULTI-TIER ARCHITECTURE - Dependency Injection Registration
// ========================================================================
// Data Services (Data Access Layer)
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.IContactDataService, SeaHaven.DataServices.Implementation.ContactDataService>();
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.IWorkOrderDataService, SeaHaven.DataServices.Implementation.WorkOrderDataService>();
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.ILocationDataService, SeaHaven.DataServices.Implementation.LocationDataService>();
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.IEmployeeDataService, SeaHaven.DataServices.Implementation.EmployeeDataService>();
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.IAccountDataService, SeaHaven.DataServices.Implementation.AccountDataService>();
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.IVendorDataService, SeaHaven.DataServices.Implementation.VendorDataService>();
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.IDispatchDataService, SeaHaven.DataServices.Implementation.DispatchDataService>();
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.IFollowUpDataService, SeaHaven.DataServices.Implementation.FollowUpDataService>();
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.IPMScheduleDataService, SeaHaven.DataServices.Implementation.PMScheduleDataService>();
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.IAssetDataService, SeaHaven.DataServices.Implementation.AssetDataService>();
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.ICategoryDataService, SeaHaven.DataServices.Implementation.CategoryDataService>();
// Business Services (Business Logic Layer)
builder.Services.AddScoped<SeaHaven.Services.Interfaces.IContactService, SeaHaven.Services.Implementation.ContactService>();
builder.Services.AddScoped<SeaHaven.Services.Interfaces.IWorkOrderService, SeaHaven.Services.Implementation.WorkOrderService>();
builder.Services.AddScoped<SeaHaven.Services.Interfaces.ILocationService, SeaHaven.Services.Implementation.LocationService>();
builder.Services.AddScoped<SeaHaven.Services.Interfaces.IEmployeeService, SeaHaven.Services.Implementation.EmployeeService>();
builder.Services.AddScoped<SeaHaven.Services.Interfaces.IAccountService, SeaHaven.Services.Implementation.AccountService>();
builder.Services.AddScoped<SeaHaven.Services.Interfaces.IVendorService, SeaHaven.Services.Implementation.VendorService>();
builder.Services.AddScoped<SeaHaven.Services.Interfaces.IDispatchService, SeaHaven.Services.Implementation.DispatchService>();
builder.Services.AddScoped<SeaHaven.Services.Interfaces.IFollowUpService, SeaHaven.Services.Implementation.FollowUpService>();
builder.Services.AddScoped<SeaHaven.Services.Interfaces.IPMScheduleService, SeaHaven.Services.Implementation.PMScheduleService>();
builder.Services.AddScoped<SeaHaven.Services.Interfaces.IAssetService, SeaHaven.Services.Implementation.AssetService>();
// ========================================================================
builder.Services.AddDataServices();
builder.Services.AddBusinessServices();
builder.Services.AddAuthentication(options =>
{

531
BACKEND_ARCHITECTURE.md Normal file
View file

@ -0,0 +1,531 @@
# 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!

View file

@ -1,553 +0,0 @@
/*
===================================================================================
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;
using Data.SeaHavenIndustries;
using Microsoft.AspNetCore.Authorization;
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
[ApiController]
[Route("api/Calendar")]
public class CalendarController : Controller
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly ApplicationDbContext _db;
public CalendarController(UserManager<ApplicationUser> userManager, ApplicationDbContext db)
{
_userManager = userManager;
_db = db;
}
// ===================================================================================
// CREATE EVENT
// ===================================================================================
[HttpPost]
[Route("AddEvent")]
public async Task<IActionResult> AddEvent(EditEvent_DTO eventDto)
{
try
{
var model = new Events
{
Title = eventDto.Title,
Description = eventDto.Description,
Location = eventDto.Location,
RecordID = eventDto.RecordID,
RecordName = eventDto.RecordName,
RecordTargetType = eventDto.RecordTargetType,
ReminderMinutes = eventDto.ReminderMinutes,
EventColor = eventDto.EventColor,
StartDate = eventDto.StartDate,
StartTime = eventDto.StartTime,
EndDate = eventDto.EndDate,
EndTime = eventDto.EndTime,
AllDay = eventDto.AllDay,
CreatedDate = DateTime.Now,
IsDeleted = false
};
_db.Events.Add(model);
await _db.SaveChangesAsync();
return Ok(new DataResponse { Message = "Event Created Successfully", Status = "200" });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
// ===================================================================================
// UPDATE EVENT
// ===================================================================================
[HttpPost]
[Route("EditEvent")]
public async Task<IActionResult> EditEvent(EditEvent_DTO eventDto)
{
try
{
if (!eventDto.Id.HasValue)
{
return BadRequest(new Response { Status = "Error", Message = "Event ID is required for editing" });
}
var model = await _db.Events.FindAsync(eventDto.Id.Value);
if (model == null || model.IsDeleted == true)
{
return NotFound(new Response { Status = "Error", Message = "Event not found" });
}
model.Title = eventDto.Title;
model.Description = eventDto.Description;
model.Location = eventDto.Location;
model.RecordID = eventDto.RecordID;
model.RecordName = eventDto.RecordName;
model.RecordTargetType = eventDto.RecordTargetType;
model.ReminderMinutes = eventDto.ReminderMinutes;
model.EventColor = eventDto.EventColor;
model.StartDate = eventDto.StartDate;
model.StartTime = eventDto.StartTime;
model.EndDate = eventDto.EndDate;
model.EndTime = eventDto.EndTime;
model.AllDay = eventDto.AllDay;
model.LastModificationTime = DateTime.Now;
_db.Events.Update(model);
await _db.SaveChangesAsync();
return Ok(new DataResponse { Message = "Event Updated Successfully", Status = "200" });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
// ===================================================================================
// GET EVENT BY ID
// ===================================================================================
[HttpGet]
[Route("GetEventById/{id}")]
public async Task<IActionResult> GetEventById(int id)
{
try
{
var model = await _db.Events
.Where(e => e.Id == id && e.IsDeleted != true)
.FirstOrDefaultAsync();
if (model == null)
{
return NotFound(new Response { Status = "Error", Message = "Event not found" });
}
var dto = new Event_DTO
{
Id = model.Id,
Title = model.Title,
Description = model.Description,
Location = model.Location,
RecordID = model.RecordID,
RecordName = model.RecordName,
RecordTargetType = model.RecordTargetType,
ReminderMinutes = model.ReminderMinutes,
EventColor = model.EventColor,
StartDate = model.StartDate,
StartTime = model.StartTime,
EndDate = model.EndDate,
EndTime = model.EndTime,
AllDay = model.AllDay,
CreatedDate = model.CreatedDate,
createdby = model.createdby
};
return Ok(new DataResponse { Message = "Success", Status = "200", Data = dto });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
// ===================================================================================
// DELETE EVENT
// ===================================================================================
[HttpDelete]
[Route("DeleteEvent/{id}")]
public async Task<IActionResult> DeleteEvent(int id)
{
try
{
var model = await _db.Events.FindAsync(id);
if (model == null)
{
return NotFound(new Response { Status = "Error", Message = "Event not found" });
}
// Soft delete
model.IsDeleted = true;
model.DeletionTime = DateTime.Now;
_db.Events.Update(model);
await _db.SaveChangesAsync();
return Ok(new DataResponse { Message = "Event Deleted Successfully", Status = "200" });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
// ===================================================================================
// GET EVENTS LIST (WITH PAGINATION AND FILTERS)
// ===================================================================================
[HttpPost]
[Route("GetEventList")]
public IActionResult GetEventList(Pagination_DTO dto)
{
try
{
var query = _db.Events.Where(e => e.IsDeleted != true);
// Apply search filter if provided
if (!string.IsNullOrWhiteSpace(dto.Search))
{
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))
);
}
// IMPORTANT: Calculate totalCount AFTER applying filters
var totalCount = query.Count();
// Apply pagination
var events = query
.OrderByDescending(e => e.StartDate)
.ThenByDescending(e => e.CreatedDate)
.Skip((dto.PageNumber - 1) * dto.PageSize)
.Take(dto.PageSize)
.Select(e => new Event_DTO
{
Id = e.Id,
Title = e.Title,
Description = e.Description,
Location = e.Location,
RecordID = e.RecordID,
RecordName = e.RecordName,
RecordTargetType = e.RecordTargetType,
ReminderMinutes = e.ReminderMinutes,
EventColor = e.EventColor,
StartDate = e.StartDate,
StartTime = e.StartTime,
EndDate = e.EndDate,
EndTime = e.EndTime,
AllDay = e.AllDay,
CreatedDate = e.CreatedDate,
createdby = e.createdby
})
.ToList();
return Ok(new DataResponse
{
Message = "Success",
Status = "200",
Data = new { rows = events, totalCount }
});
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
// ===================================================================================
// GET EVENTS FOR CALENDAR (Date Range Filter)
// ===================================================================================
[HttpGet]
[Route("GetEventsForCalendar")]
public IActionResult GetEventsForCalendar([FromQuery] DateTime? start, [FromQuery] DateTime? end)
{
try
{
var query = _db.Events.Where(e => e.IsDeleted != true);
// Filter by date range if provided
if (start.HasValue && end.HasValue)
{
query = query.Where(e =>
(e.StartDate >= start.Value && e.StartDate <= end.Value) ||
(e.EndDate >= start.Value && e.EndDate <= end.Value) ||
(e.StartDate <= start.Value && e.EndDate >= end.Value)
);
}
var events = query
.Select(e => new Event_DTO
{
Id = e.Id,
Title = e.Title,
Description = e.Description,
Location = e.Location,
RecordID = e.RecordID,
RecordName = e.RecordName,
RecordTargetType = e.RecordTargetType,
ReminderMinutes = e.ReminderMinutes,
EventColor = e.EventColor,
StartDate = e.StartDate,
StartTime = e.StartTime,
EndDate = e.EndDate,
EndTime = e.EndTime,
AllDay = e.AllDay,
CreatedDate = e.CreatedDate,
createdby = e.createdby
})
.ToList();
return Ok(new DataResponse { Message = "Success", Status = "200", Data = events });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
}
}
/*
===================================================================================
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

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

@ -1,109 +0,0 @@
-- SQL Script to add missing columns to Locations table
-- Run this in SQL Server Management Studio
USE [db_a7141e_seahavenindustry]
GO
-- Check and add AccountId column
IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Locations' AND COLUMN_NAME = 'AccountId')
BEGIN
ALTER TABLE [dbo].[Locations] ADD [AccountId] int NULL;
PRINT 'Added AccountId column';
END
ELSE
PRINT 'AccountId column already exists';
-- Check and add Address column
IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Locations' AND COLUMN_NAME = 'Address')
BEGIN
ALTER TABLE [dbo].[Locations] ADD [Address] nvarchar(max) NULL;
PRINT 'Added Address column';
END
ELSE
PRINT 'Address column already exists';
-- Check and add City column
IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Locations' AND COLUMN_NAME = 'City')
BEGIN
ALTER TABLE [dbo].[Locations] ADD [City] nvarchar(max) NULL;
PRINT 'Added City column';
END
ELSE
PRINT 'City column already exists';
-- Check and add Contact column
IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Locations' AND COLUMN_NAME = 'Contact')
BEGIN
ALTER TABLE [dbo].[Locations] ADD [Contact] nvarchar(max) NULL;
PRINT 'Added Contact column';
END
ELSE
PRINT 'Contact column already exists';
-- Check and add ContactEmail column
IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Locations' AND COLUMN_NAME = 'ContactEmail')
BEGIN
ALTER TABLE [dbo].[Locations] ADD [ContactEmail] nvarchar(max) NULL;
PRINT 'Added ContactEmail column';
END
ELSE
PRINT 'ContactEmail column already exists';
-- Check and add Phone column
IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Locations' AND COLUMN_NAME = 'Phone')
BEGIN
ALTER TABLE [dbo].[Locations] ADD [Phone] nvarchar(max) NULL;
PRINT 'Added Phone column';
END
ELSE
PRINT 'Phone column already exists';
-- Check and add State column
IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Locations' AND COLUMN_NAME = 'State')
BEGIN
ALTER TABLE [dbo].[Locations] ADD [State] nvarchar(max) NULL;
PRINT 'Added State column';
END
ELSE
PRINT 'State column already exists';
-- Check and add Status column
IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Locations' AND COLUMN_NAME = 'Status')
BEGIN
ALTER TABLE [dbo].[Locations] ADD [Status] nvarchar(max) NULL;
PRINT 'Added Status column';
END
ELSE
PRINT 'Status column already exists';
-- Check and add ZipCode column
IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Locations' AND COLUMN_NAME = 'ZipCode')
BEGIN
ALTER TABLE [dbo].[Locations] ADD [ZipCode] nvarchar(max) NULL;
PRINT 'Added ZipCode column';
END
ELSE
PRINT 'ZipCode column already exists';
-- Check and add foreign key index
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Locations_AccountId' AND object_id = OBJECT_ID('dbo.Locations'))
BEGIN
CREATE NONCLUSTERED INDEX [IX_Locations_AccountId] ON [dbo].[Locations] ([AccountId] ASC);
PRINT 'Added IX_Locations_AccountId index';
END
ELSE
PRINT 'IX_Locations_AccountId index already exists';
-- Check and add foreign key constraint if Accounts table exists
IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'Accounts')
AND NOT EXISTS (SELECT * FROM sys.foreign_keys WHERE name = 'FK_Locations_Accounts_AccountId')
BEGIN
ALTER TABLE [dbo].[Locations] WITH CHECK ADD CONSTRAINT [FK_Locations_Accounts_AccountId]
FOREIGN KEY([AccountId]) REFERENCES [dbo].[Accounts] ([Id]);
PRINT 'Added FK_Locations_Accounts_AccountId foreign key';
END
ELSE
PRINT 'FK_Locations_Accounts_AccountId foreign key already exists or Accounts table does not exist';
PRINT 'Schema fix completed!';
GO

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

@ -111,5 +111,110 @@ namespace SeaHaven.DataServices.Implementation
{
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,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

@ -239,6 +239,89 @@ namespace SeaHaven.DataServices.Implementation
.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);
@ -270,5 +353,15 @@ namespace SeaHaven.DataServices.Implementation
{
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,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

@ -8,6 +8,8 @@ namespace SeaHaven.DataServices.Interfaces
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);
@ -15,6 +17,16 @@ namespace SeaHaven.DataServices.Interfaces
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,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

@ -38,22 +38,35 @@ namespace SeaHaven.DataServices.Interfaces
Task UpdateAsync(WorkOrder workOrder);
Task DeleteAsync(int id);
// Related entities
// 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

@ -1,8 +1,5 @@
namespace SeaHaven.Services.DTOs
{
/// <summary>
/// Contact DTO for responses
/// </summary>
public class ContactDTO
{
public int Id { get; set; }
@ -21,9 +18,6 @@ namespace SeaHaven.Services.DTOs
public string? ModifiedBy { get; set; }
}
/// <summary>
/// DTO for creating a new contact
/// </summary>
public class CreateContactDTO
{
public required string FirstName { get; set; }
@ -36,9 +30,6 @@ namespace SeaHaven.Services.DTOs
public int? AccountId { get; set; }
}
/// <summary>
/// DTO for updating an existing contact
/// </summary>
public class UpdateContactDTO
{
public string? FirstName { get; set; }
@ -51,9 +42,6 @@ namespace SeaHaven.Services.DTOs
public int? AccountId { get; set; }
}
/// <summary>
/// Paged result wrapper
/// </summary>
public class PagedResult<T>
{
public IEnumerable<T> Items { get; set; } = new List<T>();

View file

@ -1,3 +1,6 @@
using Data.SeaHavenIndustries;
using Data.SeaHavenIndustries.Enums;
namespace SeaHaven.Services.DTOs
{
public class WorkOrderDTO
@ -6,8 +9,8 @@ namespace SeaHaven.Services.DTOs
public string? InternalWONumber { get; set; }
public string? WorkerOrderTitle { get; set; }
public string? Description { get; set; }
public string? Status { get; set; }
public string? Priority { 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; }
@ -17,28 +20,110 @@ namespace SeaHaven.Services.DTOs
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 string? Status { get; set; }
public string? Priority { 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 string? Status { get; set; }
public string? Priority { 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

@ -1,7 +1,9 @@
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
{
@ -12,10 +14,17 @@ namespace SeaHaven.Services.Implementation
public class AccountService : IAccountService
{
private readonly IAccountDataService _accountDataService;
private readonly ICreateAccountValidation _createValidator;
private readonly IUpdateAccountValidation _updateValidator;
public AccountService(IAccountDataService accountDataService)
public AccountService(
IAccountDataService accountDataService,
ICreateAccountValidation createValidator,
IUpdateAccountValidation updateValidator)
{
_accountDataService = accountDataService;
_createValidator = createValidator;
_updateValidator = updateValidator;
}
public async Task<AccountDTO?> GetAccountByIdAsync(int id)
@ -50,6 +59,12 @@ namespace SeaHaven.Services.Implementation
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,
@ -66,6 +81,12 @@ namespace SeaHaven.Services.Implementation
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");

View file

@ -1,7 +1,9 @@
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
{
@ -12,10 +14,17 @@ namespace SeaHaven.Services.Implementation
public class AssetService : IAssetService
{
private readonly IAssetDataService _assetDataService;
private readonly ICreateAssetValidation _createValidator;
private readonly IUpdateAssetValidation _updateValidator;
public AssetService(IAssetDataService assetDataService)
public AssetService(
IAssetDataService assetDataService,
ICreateAssetValidation createValidator,
IUpdateAssetValidation updateValidator)
{
_assetDataService = assetDataService;
_createValidator = createValidator;
_updateValidator = updateValidator;
}
public async Task<AssetDTO?> GetAssetByIdAsync(int id)
@ -56,6 +65,12 @@ namespace SeaHaven.Services.Implementation
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,
@ -70,6 +85,12 @@ namespace SeaHaven.Services.Implementation
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");

View file

@ -1,7 +1,9 @@
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
{
@ -12,10 +14,17 @@ namespace SeaHaven.Services.Implementation
public class ContactService : IContactService
{
private readonly IContactDataService _contactDataService;
private readonly ICreateContactValidation _createValidator;
private readonly IUpdateContactValidation _updateValidator;
public ContactService(IContactDataService contactDataService)
public ContactService(
IContactDataService contactDataService,
ICreateContactValidation createValidator,
IUpdateContactValidation updateValidator)
{
_contactDataService = contactDataService;
_createValidator = createValidator;
_updateValidator = updateValidator;
}
// Query operations
@ -76,20 +85,21 @@ namespace SeaHaven.Services.Implementation
// Command operations
public async Task<ContactDTO> CreateContactAsync(CreateContactDTO dto, string userId)
{
// Validation
if (string.IsNullOrWhiteSpace(dto.Email))
throw new ArgumentException("Email is required", nameof(dto.Email));
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");
// Map DTO to entity
var contact = new Contacts
{
FirstName = dto.FirstName,
LastName = dto.LastName,
Email = dto.Email,
PhoneNumber = dto.Phone ?? dto.Mobile, // Contacts only has PhoneNumber, not separate Mobile
PhoneNumber = dto.Phone ?? dto.Mobile,
Title = dto.Title,
ContactType = dto.Type,
AccountId = dto.AccountId,
@ -97,7 +107,6 @@ namespace SeaHaven.Services.Implementation
CreatedDate = DateTime.Now
};
// Save
var savedContact = await _contactDataService.AddAsync(contact);
return MapToDTO(savedContact);
@ -105,19 +114,22 @@ namespace SeaHaven.Services.Implementation
public async Task<ContactDTO> UpdateContactAsync(int id, UpdateContactDTO dto, string userId)
{
// Check if exists
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");
// Validate email uniqueness if changed
if (!string.IsNullOrWhiteSpace(dto.Email) && dto.Email != existingContact.Email)
{
if (await _contactDataService.EmailExistsAsync(dto.Email, id))
throw new InvalidOperationException($"Email '{dto.Email}' already exists");
}
// Update fields
if (!string.IsNullOrWhiteSpace(dto.FirstName))
existingContact.FirstName = dto.FirstName;
@ -128,7 +140,7 @@ namespace SeaHaven.Services.Implementation
existingContact.Email = dto.Email;
if (dto.Phone != null || dto.Mobile != null)
existingContact.PhoneNumber = dto.Phone ?? dto.Mobile; // Contacts only has PhoneNumber
existingContact.PhoneNumber = dto.Phone ?? dto.Mobile;
if (dto.Title != null)
existingContact.Title = dto.Title;
@ -141,7 +153,6 @@ namespace SeaHaven.Services.Implementation
existingContact.LastModificationTime = DateTime.Now;
// Save
await _contactDataService.UpdateAsync(existingContact);
return MapToDTO(existingContact);

View file

@ -1,7 +1,9 @@
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
{
@ -12,10 +14,17 @@ namespace SeaHaven.Services.Implementation
public class DispatchService : IDispatchService
{
private readonly IDispatchDataService _dispatchDataService;
private readonly ICreateDispatchValidation _createValidator;
private readonly IUpdateDispatchValidation _updateValidator;
public DispatchService(IDispatchDataService dispatchDataService)
public DispatchService(
IDispatchDataService dispatchDataService,
ICreateDispatchValidation createValidator,
IUpdateDispatchValidation updateValidator)
{
_dispatchDataService = dispatchDataService;
_createValidator = createValidator;
_updateValidator = updateValidator;
}
public async Task<DispatchDTO?> GetDispatchByIdAsync(int id)
@ -56,6 +65,12 @@ namespace SeaHaven.Services.Implementation
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
@ -74,6 +89,12 @@ namespace SeaHaven.Services.Implementation
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");

View file

@ -1,7 +1,9 @@
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
{
@ -12,10 +14,17 @@ namespace SeaHaven.Services.Implementation
public class EmployeeService : IEmployeeService
{
private readonly IEmployeeDataService _employeeDataService;
private readonly ICreateEmployeeValidation _createValidator;
private readonly IUpdateEmployeeValidation _updateValidator;
public EmployeeService(IEmployeeDataService employeeDataService)
public EmployeeService(
IEmployeeDataService employeeDataService,
ICreateEmployeeValidation createValidator,
IUpdateEmployeeValidation updateValidator)
{
_employeeDataService = employeeDataService;
_createValidator = createValidator;
_updateValidator = updateValidator;
}
// Query operations
@ -51,6 +60,12 @@ namespace SeaHaven.Services.Implementation
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,
@ -86,6 +101,12 @@ namespace SeaHaven.Services.Implementation
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");

View file

@ -1,7 +1,9 @@
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
{
@ -12,10 +14,17 @@ namespace SeaHaven.Services.Implementation
public class FollowUpService : IFollowUpService
{
private readonly IFollowUpDataService _followUpDataService;
private readonly ICreateFollowUpValidation _createValidator;
private readonly IUpdateFollowUpValidation _updateValidator;
public FollowUpService(IFollowUpDataService followUpDataService)
public FollowUpService(
IFollowUpDataService followUpDataService,
ICreateFollowUpValidation createValidator,
IUpdateFollowUpValidation updateValidator)
{
_followUpDataService = followUpDataService;
_createValidator = createValidator;
_updateValidator = updateValidator;
}
public async Task<FollowUpDTO?> GetFollowUpByIdAsync(int id)
@ -63,6 +72,12 @@ namespace SeaHaven.Services.Implementation
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,
@ -83,6 +98,12 @@ namespace SeaHaven.Services.Implementation
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");

View file

@ -1,7 +1,9 @@
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
{
@ -12,10 +14,17 @@ namespace SeaHaven.Services.Implementation
public class LocationService : ILocationService
{
private readonly ILocationDataService _locationDataService;
private readonly ICreateLocationValidation _createValidator;
private readonly IUpdateLocationValidation _updateValidator;
public LocationService(ILocationDataService locationDataService)
public LocationService(
ILocationDataService locationDataService,
ICreateLocationValidation createValidator,
IUpdateLocationValidation updateValidator)
{
_locationDataService = locationDataService;
_createValidator = createValidator;
_updateValidator = updateValidator;
}
// Query operations
@ -57,6 +66,12 @@ namespace SeaHaven.Services.Implementation
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,
@ -75,6 +90,12 @@ namespace SeaHaven.Services.Implementation
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");

View file

@ -1,7 +1,9 @@
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
{
@ -12,10 +14,17 @@ namespace SeaHaven.Services.Implementation
public class PMScheduleService : IPMScheduleService
{
private readonly IPMScheduleDataService _pmScheduleDataService;
private readonly ICreatePMScheduleValidation _createValidator;
private readonly IUpdatePMScheduleValidation _updateValidator;
public PMScheduleService(IPMScheduleDataService pmScheduleDataService)
public PMScheduleService(
IPMScheduleDataService pmScheduleDataService,
ICreatePMScheduleValidation createValidator,
IUpdatePMScheduleValidation updateValidator)
{
_pmScheduleDataService = pmScheduleDataService;
_createValidator = createValidator;
_updateValidator = updateValidator;
}
public async Task<PMScheduleDTO?> GetPMScheduleByIdAsync(int id)
@ -56,6 +65,12 @@ namespace SeaHaven.Services.Implementation
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,
@ -74,6 +89,12 @@ namespace SeaHaven.Services.Implementation
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");

View file

@ -1,7 +1,9 @@
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
{
@ -12,10 +14,17 @@ namespace SeaHaven.Services.Implementation
public class VendorService : IVendorService
{
private readonly IVendorDataService _vendorDataService;
private readonly ICreateVendorValidation _createValidator;
private readonly IUpdateVendorValidation _updateValidator;
public VendorService(IVendorDataService vendorDataService)
public VendorService(
IVendorDataService vendorDataService,
ICreateVendorValidation createValidator,
IUpdateVendorValidation updateValidator)
{
_vendorDataService = vendorDataService;
_createValidator = createValidator;
_updateValidator = updateValidator;
}
public async Task<VendorDTO?> GetVendorByIdAsync(int id)
@ -50,6 +59,12 @@ namespace SeaHaven.Services.Implementation
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,
@ -69,6 +84,12 @@ namespace SeaHaven.Services.Implementation
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");

View file

@ -1,35 +1,45 @@
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)
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 : MapToDTO(workOrder);
return workOrder == null ? null : WorkOrderDTO.MapToModel(workOrder);
}
public async Task<WorkOrderDTO?> GetWorkOrderByIdWithDetailsAsync(int id)
{
var workOrder = await _workOrderDataService.GetByIdWithDetailsAsync(id);
return workOrder == null ? null : MapToDTO(workOrder);
return workOrder == null ? null : WorkOrderDTO.MapToModel(workOrder);
}
public async Task<IEnumerable<WorkOrderDTO>> GetAllWorkOrdersAsync()
{
var workOrders = await _workOrderDataService.GetAllAsync();
return workOrders.Select(MapToDTO);
return workOrders.Select(WorkOrderDTO.MapToModel);
}
public async Task<PagedResult<WorkOrderDTO>> GetWorkOrdersPagedAsync(int page, int pageSize, string? search = null, string? status = null, string? priority = null)
@ -37,69 +47,71 @@ namespace SeaHaven.Services.Implementation
var (items, totalCount) = await _workOrderDataService.GetPagedAsync(page, pageSize, search, status, priority);
return new PagedResult<WorkOrderDTO>
{
Items = items.Select(MapToDTO),
Items = items.Select(WorkOrderDTO.MapToModel),
TotalCount = totalCount,
Page = page,
PageSize = pageSize
};
}
public async Task<WorkOrderDTO> CreateWorkOrderAsync(CreateWorkOrderDTO dto, string userId)
public async Task<int> CreateWorkOrderAsync(CreateWorkOrderDTO dto, string userId)
{
var workOrder = new WorkOrder
var validationResult = await _createValidator.ValidateAsync(dto);
if (!validationResult.IsValid)
{
WorkerOrderTitle = dto.WorkerOrderTitle,
Description = dto.Description,
Status = dto.Status ?? "Open",
Priority = dto.Priority,
LocationId = dto.LocationId,
AssignTo = dto.AssignTo,
DueDate = dto.DueDate,
createdby = userId,
CreatedDate = DateTime.Now
};
throw new ValidationException(validationResult.Errors);
}
// Generate internal number
var workOrder = CreateWorkOrderDTO.MapToEntity(dto);
workOrder.createdby = userId;
workOrder.CreatedDate = DateTime.UtcNow;
workOrder.InternalWONumber = await _workOrderDataService.GenerateNextInternalWONumberAsync();
var savedWorkOrder = await _workOrderDataService.AddAsync(workOrder);
return MapToDTO(savedWorkOrder);
var saved = await _workOrderDataService.AddAsync(workOrder);
await LogAuditAsync(saved.Id, userId, AuditAction.Create, "Work order created");
return saved.Id;
}
public async Task<WorkOrderDTO> UpdateWorkOrderAsync(int id, UpdateWorkOrderDTO dto, string userId)
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)
throw new KeyNotFoundException($"WorkOrder with ID {id} not found");
return false;
if (!string.IsNullOrWhiteSpace(dto.WorkerOrderTitle))
existingWorkOrder.WorkerOrderTitle = dto.WorkerOrderTitle;
if (dto.Description != null)
existingWorkOrder.Description = dto.Description;
if (dto.Status != null)
existingWorkOrder.Status = dto.Status;
if (dto.Priority != null)
existingWorkOrder.Priority = dto.Priority;
if (dto.LocationId.HasValue)
existingWorkOrder.LocationId = dto.LocationId;
if (dto.AssignTo != null)
existingWorkOrder.AssignTo = dto.AssignTo;
if (dto.DueDate.HasValue)
existingWorkOrder.DueDate = dto.DueDate;
if (dto.CompletedDate.HasValue)
existingWorkOrder.CompletedDate = dto.CompletedDate;
existingWorkOrder.LastModificationTime = DateTime.Now;
var changes = new List<string>();
changes.AddRange(TrackChanges(existingWorkOrder, dto));
UpdateWorkOrderDTO.UpdateEntity(dto, existingWorkOrder);
await _workOrderDataService.UpdateAsync(existingWorkOrder);
return MapToDTO(existingWorkOrder);
if (changes.Any())
{
await LogAuditAsync(id, userId, AuditAction.Edit, string.Join(", ", changes));
}
return true;
}
public async Task DeleteWorkOrderAsync(int id, string userId)
public async Task<bool> DeleteWorkOrderAsync(int id, string userId)
{
if (!await _workOrderDataService.ExistsAsync(id))
throw new KeyNotFoundException($"WorkOrder with ID {id} not found");
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)
@ -117,26 +129,40 @@ namespace SeaHaven.Services.Implementation
return await _workOrderDataService.CountAsync();
}
private static WorkOrderDTO MapToDTO(WorkOrder workOrder)
private List<string> TrackChanges(WorkOrder existing, UpdateWorkOrderDTO dto)
{
return new WorkOrderDTO
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
{
Id = workOrder.Id,
InternalWONumber = workOrder.InternalWONumber,
WorkerOrderTitle = workOrder.WorkerOrderTitle,
Description = workOrder.Description,
Status = workOrder.Status,
Priority = workOrder.Priority,
LocationId = workOrder.LocationId,
LocationName = workOrder.Locations?.Name,
AssignTo = workOrder.AssignTo,
DueDate = workOrder.DueDate,
CompletedDate = workOrder.CompletedDate,
CreatedDate = workOrder.CreatedDate,
CreatedBy = workOrder.createdby,
ModifiedDate = workOrder.LastModificationTime,
ModifiedBy = null // Not tracked in FullAuditEntity
WorkOrderId = workOrderId,
Action = action.ToString(),
FieldName = description,
UserId = userId,
CreatedAt = DateTime.UtcNow
};
await _workOrderDataService.AddAuditLogAsync(auditLog);
}
}
}
}

View file

@ -8,9 +8,9 @@ namespace SeaHaven.Services.Interfaces
Task<WorkOrderDTO?> GetWorkOrderByIdWithDetailsAsync(int id);
Task<IEnumerable<WorkOrderDTO>> GetAllWorkOrdersAsync();
Task<PagedResult<WorkOrderDTO>> GetWorkOrdersPagedAsync(int page, int pageSize, string? search = null, string? status = null, string? priority = null);
Task<WorkOrderDTO> CreateWorkOrderAsync(CreateWorkOrderDTO dto, string userId);
Task<WorkOrderDTO> UpdateWorkOrderAsync(int id, UpdateWorkOrderDTO dto, string userId);
Task DeleteWorkOrderAsync(int id, string userId);
Task<int> CreateWorkOrderAsync(CreateWorkOrderDTO dto, string userId);
Task<bool> UpdateWorkOrderAsync(int id, UpdateWorkOrderDTO dto, string userId);
Task<bool> DeleteWorkOrderAsync(int id, string userId);
Task<bool> WorkOrderExistsAsync(int id);
Task<string> GenerateNextInternalWONumberAsync();
Task<int> GetTotalWorkOrderCountAsync();

View file

@ -11,4 +11,9 @@
<ProjectReference Include="..\Data.SeaHavenIndustries\Data.SeaHavenIndustries.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentValidation" Version="12.1.1" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,57 @@
using FluentValidation;
using SeaHaven.Services.DTOs;
namespace SeaHaven.Services.Validation
{
public interface ICreateAccountValidation : IValidator<CreateAccountDTO>
{
}
public class CreateAccountValidation : AbstractValidator<CreateAccountDTO>, ICreateAccountValidation
{
public CreateAccountValidation()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Account name is required")
.MaximumLength(200).WithMessage("Account name cannot exceed 200 characters");
RuleFor(x => x.Email)
.EmailAddress().WithMessage("Invalid email address")
.When(x => !string.IsNullOrEmpty(x.Email));
RuleFor(x => x.Phone)
.MaximumLength(20).WithMessage("Phone number cannot exceed 20 characters")
.When(x => !string.IsNullOrEmpty(x.Phone));
RuleFor(x => x.Website)
.MaximumLength(200).WithMessage("Website cannot exceed 200 characters")
.When(x => !string.IsNullOrEmpty(x.Website));
}
}
public interface IUpdateAccountValidation : IValidator<UpdateAccountDTO>
{
}
public class UpdateAccountValidation : AbstractValidator<UpdateAccountDTO>, IUpdateAccountValidation
{
public UpdateAccountValidation()
{
RuleFor(x => x.Name)
.MaximumLength(200).WithMessage("Account name cannot exceed 200 characters")
.When(x => !string.IsNullOrEmpty(x.Name));
RuleFor(x => x.Email)
.EmailAddress().WithMessage("Invalid email address")
.When(x => !string.IsNullOrEmpty(x.Email));
RuleFor(x => x.Phone)
.MaximumLength(20).WithMessage("Phone number cannot exceed 20 characters")
.When(x => !string.IsNullOrEmpty(x.Phone));
RuleFor(x => x.Website)
.MaximumLength(200).WithMessage("Website cannot exceed 200 characters")
.When(x => !string.IsNullOrEmpty(x.Website));
}
}
}

View file

@ -0,0 +1,41 @@
using FluentValidation;
using SeaHaven.Services.DTOs;
namespace SeaHaven.Services.Validation
{
public interface ICreateAssetValidation : IValidator<CreateAssetDTO>
{
}
public class CreateAssetValidation : AbstractValidator<CreateAssetDTO>, ICreateAssetValidation
{
public CreateAssetValidation()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Asset name is required")
.MaximumLength(200).WithMessage("Asset name cannot exceed 200 characters");
RuleFor(x => x.LocationId)
.GreaterThan(0).WithMessage("Valid location must be selected")
.When(x => x.LocationId.HasValue);
}
}
public interface IUpdateAssetValidation : IValidator<UpdateAssetDTO>
{
}
public class UpdateAssetValidation : AbstractValidator<UpdateAssetDTO>, IUpdateAssetValidation
{
public UpdateAssetValidation()
{
RuleFor(x => x.Name)
.MaximumLength(200).WithMessage("Asset name cannot exceed 200 characters")
.When(x => !string.IsNullOrEmpty(x.Name));
RuleFor(x => x.LocationId)
.GreaterThan(0).WithMessage("Valid location must be selected")
.When(x => x.LocationId.HasValue);
}
}
}

View file

@ -0,0 +1,73 @@
using FluentValidation;
using SeaHaven.Services.DTOs;
namespace SeaHaven.Services.Validation
{
public interface ICreateContactValidation : IValidator<CreateContactDTO>
{
}
public class CreateContactValidation : AbstractValidator<CreateContactDTO>, ICreateContactValidation
{
public CreateContactValidation()
{
RuleFor(x => x.FirstName)
.NotEmpty().WithMessage("First name is required")
.MaximumLength(100).WithMessage("First name cannot exceed 100 characters");
RuleFor(x => x.LastName)
.NotEmpty().WithMessage("Last name is required")
.MaximumLength(100).WithMessage("Last name cannot exceed 100 characters");
RuleFor(x => x.Email)
.NotEmpty().WithMessage("Email is required")
.EmailAddress().WithMessage("Invalid email address");
RuleFor(x => x.Phone)
.MaximumLength(20).WithMessage("Phone number cannot exceed 20 characters")
.When(x => !string.IsNullOrEmpty(x.Phone));
RuleFor(x => x.Mobile)
.MaximumLength(20).WithMessage("Mobile number cannot exceed 20 characters")
.When(x => !string.IsNullOrEmpty(x.Mobile));
RuleFor(x => x.AccountId)
.GreaterThan(0).WithMessage("Valid account must be selected")
.When(x => x.AccountId.HasValue);
}
}
public interface IUpdateContactValidation : IValidator<UpdateContactDTO>
{
}
public class UpdateContactValidation : AbstractValidator<UpdateContactDTO>, IUpdateContactValidation
{
public UpdateContactValidation()
{
RuleFor(x => x.FirstName)
.MaximumLength(100).WithMessage("First name cannot exceed 100 characters")
.When(x => !string.IsNullOrEmpty(x.FirstName));
RuleFor(x => x.LastName)
.MaximumLength(100).WithMessage("Last name cannot exceed 100 characters")
.When(x => !string.IsNullOrEmpty(x.LastName));
RuleFor(x => x.Email)
.EmailAddress().WithMessage("Invalid email address")
.When(x => !string.IsNullOrEmpty(x.Email));
RuleFor(x => x.Phone)
.MaximumLength(20).WithMessage("Phone number cannot exceed 20 characters")
.When(x => !string.IsNullOrEmpty(x.Phone));
RuleFor(x => x.Mobile)
.MaximumLength(20).WithMessage("Mobile number cannot exceed 20 characters")
.When(x => !string.IsNullOrEmpty(x.Mobile));
RuleFor(x => x.AccountId)
.GreaterThan(0).WithMessage("Valid account must be selected")
.When(x => x.AccountId.HasValue);
}
}
}

View file

@ -0,0 +1,44 @@
using FluentValidation;
using SeaHaven.Services.DTOs;
using Data.SeaHavenIndustries.Enums;
namespace SeaHaven.Services.Validation
{
public interface ICreateDispatchValidation : IValidator<CreateDispatchDTO>
{
}
public class CreateDispatchValidation : AbstractValidator<CreateDispatchDTO>, ICreateDispatchValidation
{
public CreateDispatchValidation()
{
RuleFor(x => x.VendorId)
.NotEmpty().WithMessage("Vendor is required")
.GreaterThan(0).WithMessage("Valid vendor must be selected");
RuleFor(x => x.ScheduledDate)
.GreaterThan(DateTime.Now.AddDays(-1))
.When(x => x.ScheduledDate.HasValue)
.WithMessage("Scheduled date cannot be in the past");
}
}
public interface IUpdateDispatchValidation : IValidator<UpdateDispatchDTO>
{
}
public class UpdateDispatchValidation : AbstractValidator<UpdateDispatchDTO>, IUpdateDispatchValidation
{
public UpdateDispatchValidation()
{
RuleFor(x => x.VendorId)
.GreaterThan(0).WithMessage("Valid vendor must be selected")
.When(x => x.VendorId.HasValue);
RuleFor(x => x.ScheduledDate)
.GreaterThan(DateTime.Now.AddDays(-1))
.When(x => x.ScheduledDate.HasValue)
.WithMessage("Scheduled date cannot be in the past");
}
}
}

View file

@ -0,0 +1,81 @@
using FluentValidation;
using SeaHaven.Services.DTOs;
namespace SeaHaven.Services.Validation
{
public interface ICreateEmployeeValidation : IValidator<CreateEmployeeDTO>
{
}
public class CreateEmployeeValidation : AbstractValidator<CreateEmployeeDTO>, ICreateEmployeeValidation
{
public CreateEmployeeValidation()
{
RuleFor(x => x.FirstName)
.NotEmpty().WithMessage("First name is required")
.MaximumLength(100).WithMessage("First name cannot exceed 100 characters");
RuleFor(x => x.LastName)
.NotEmpty().WithMessage("Last name is required")
.MaximumLength(100).WithMessage("Last name cannot exceed 100 characters");
RuleFor(x => x.Email)
.EmailAddress().WithMessage("Invalid email address")
.When(x => !string.IsNullOrEmpty(x.Email));
RuleFor(x => x.Phone)
.MaximumLength(20).WithMessage("Phone number cannot exceed 20 characters")
.When(x => !string.IsNullOrEmpty(x.Phone));
RuleFor(x => x.DepartmentId)
.GreaterThan(0).WithMessage("Valid department must be selected")
.When(x => x.DepartmentId.HasValue);
RuleFor(x => x.JobTitleId)
.GreaterThan(0).WithMessage("Valid job title must be selected")
.When(x => x.JobTitleId.HasValue);
RuleFor(x => x.RegionId)
.GreaterThan(0).WithMessage("Valid region must be selected")
.When(x => x.RegionId.HasValue);
}
}
public interface IUpdateEmployeeValidation : IValidator<UpdateEmployeeDTO>
{
}
public class UpdateEmployeeValidation : AbstractValidator<UpdateEmployeeDTO>, IUpdateEmployeeValidation
{
public UpdateEmployeeValidation()
{
RuleFor(x => x.FirstName)
.MaximumLength(100).WithMessage("First name cannot exceed 100 characters")
.When(x => !string.IsNullOrEmpty(x.FirstName));
RuleFor(x => x.LastName)
.MaximumLength(100).WithMessage("Last name cannot exceed 100 characters")
.When(x => !string.IsNullOrEmpty(x.LastName));
RuleFor(x => x.Email)
.EmailAddress().WithMessage("Invalid email address")
.When(x => !string.IsNullOrEmpty(x.Email));
RuleFor(x => x.Phone)
.MaximumLength(20).WithMessage("Phone number cannot exceed 20 characters")
.When(x => !string.IsNullOrEmpty(x.Phone));
RuleFor(x => x.DepartmentId)
.GreaterThan(0).WithMessage("Valid department must be selected")
.When(x => x.DepartmentId.HasValue);
RuleFor(x => x.JobTitleId)
.GreaterThan(0).WithMessage("Valid job title must be selected")
.When(x => x.JobTitleId.HasValue);
RuleFor(x => x.RegionId)
.GreaterThan(0).WithMessage("Valid region must be selected")
.When(x => x.RegionId.HasValue);
}
}
}

View file

@ -0,0 +1,78 @@
using FluentValidation;
using SeaHaven.Services.DTOs;
using Data.SeaHavenIndustries.Enums;
namespace SeaHaven.Services.Validation
{
public interface ICreateFollowUpValidation : IValidator<CreateFollowUpDTO>
{
}
public class CreateFollowUpValidation : AbstractValidator<CreateFollowUpDTO>, ICreateFollowUpValidation
{
public CreateFollowUpValidation()
{
RuleFor(x => x.Reason)
.NotEmpty().WithMessage("Reason is required")
.MaximumLength(500).WithMessage("Reason cannot exceed 500 characters");
RuleFor(x => x.Status)
.NotEmpty().WithMessage("Status is required");
RuleFor(x => x.ScheduleDate)
.GreaterThan(DateTime.Now.AddDays(-1))
.WithMessage("Schedule date cannot be in the past");
RuleFor(x => x.EmployeeId)
.GreaterThan(0).WithMessage("Valid employee must be selected")
.When(x => x.EmployeeId.HasValue);
RuleFor(x => x.WorkOrderId)
.GreaterThan(0).WithMessage("Valid work order must be selected")
.When(x => x.WorkOrderId.HasValue);
RuleFor(x => x.AccountId)
.GreaterThan(0).WithMessage("Valid account must be selected")
.When(x => x.AccountId.HasValue);
RuleFor(x => x.LocationId)
.GreaterThan(0).WithMessage("Valid location must be selected")
.When(x => x.LocationId.HasValue);
}
}
public interface IUpdateFollowUpValidation : IValidator<UpdateFollowUpDTO>
{
}
public class UpdateFollowUpValidation : AbstractValidator<UpdateFollowUpDTO>, IUpdateFollowUpValidation
{
public UpdateFollowUpValidation()
{
RuleFor(x => x.Reason)
.MaximumLength(500).WithMessage("Reason cannot exceed 500 characters")
.When(x => !string.IsNullOrEmpty(x.Reason));
RuleFor(x => x.ScheduleDate)
.GreaterThan(DateTime.Now.AddDays(-1))
.When(x => x.ScheduleDate.HasValue)
.WithMessage("Schedule date cannot be in the past");
RuleFor(x => x.EmployeeId)
.GreaterThan(0).WithMessage("Valid employee must be selected")
.When(x => x.EmployeeId.HasValue);
RuleFor(x => x.WorkOrderId)
.GreaterThan(0).WithMessage("Valid work order must be selected")
.When(x => x.WorkOrderId.HasValue);
RuleFor(x => x.AccountId)
.GreaterThan(0).WithMessage("Valid account must be selected")
.When(x => x.AccountId.HasValue);
RuleFor(x => x.LocationId)
.GreaterThan(0).WithMessage("Valid location must be selected")
.When(x => x.LocationId.HasValue);
}
}
}

View file

@ -0,0 +1,73 @@
using FluentValidation;
using SeaHaven.Services.DTOs;
namespace SeaHaven.Services.Validation
{
public interface ICreateLocationValidation : IValidator<CreateLocationDTO>
{
}
public class CreateLocationValidation : AbstractValidator<CreateLocationDTO>, ICreateLocationValidation
{
public CreateLocationValidation()
{
RuleFor(x => x.LocationName)
.NotEmpty().WithMessage("Location name is required")
.MaximumLength(200).WithMessage("Location name cannot exceed 200 characters");
RuleFor(x => x.Address)
.MaximumLength(500).WithMessage("Address cannot exceed 500 characters")
.When(x => !string.IsNullOrEmpty(x.Address));
RuleFor(x => x.City)
.MaximumLength(100).WithMessage("City cannot exceed 100 characters")
.When(x => !string.IsNullOrEmpty(x.City));
RuleFor(x => x.State)
.MaximumLength(2).WithMessage("State must be 2 characters")
.When(x => !string.IsNullOrEmpty(x.State));
RuleFor(x => x.Zipcode)
.MaximumLength(10).WithMessage("Zipcode cannot exceed 10 characters")
.When(x => !string.IsNullOrEmpty(x.Zipcode));
RuleFor(x => x.AccountId)
.GreaterThan(0).WithMessage("Valid account must be selected")
.When(x => x.AccountId.HasValue);
}
}
public interface IUpdateLocationValidation : IValidator<UpdateLocationDTO>
{
}
public class UpdateLocationValidation : AbstractValidator<UpdateLocationDTO>, IUpdateLocationValidation
{
public UpdateLocationValidation()
{
RuleFor(x => x.LocationName)
.MaximumLength(200).WithMessage("Location name cannot exceed 200 characters")
.When(x => !string.IsNullOrEmpty(x.LocationName));
RuleFor(x => x.Address)
.MaximumLength(500).WithMessage("Address cannot exceed 500 characters")
.When(x => !string.IsNullOrEmpty(x.Address));
RuleFor(x => x.City)
.MaximumLength(100).WithMessage("City cannot exceed 100 characters")
.When(x => !string.IsNullOrEmpty(x.City));
RuleFor(x => x.State)
.MaximumLength(2).WithMessage("State must be 2 characters")
.When(x => !string.IsNullOrEmpty(x.State));
RuleFor(x => x.Zipcode)
.MaximumLength(10).WithMessage("Zipcode cannot exceed 10 characters")
.When(x => !string.IsNullOrEmpty(x.Zipcode));
RuleFor(x => x.AccountId)
.GreaterThan(0).WithMessage("Valid account must be selected")
.When(x => x.AccountId.HasValue);
}
}
}

View file

@ -0,0 +1,62 @@
using FluentValidation;
using SeaHaven.Services.DTOs;
namespace SeaHaven.Services.Validation
{
public interface ICreatePMScheduleValidation : IValidator<CreatePMScheduleDTO>
{
}
public class CreatePMScheduleValidation : AbstractValidator<CreatePMScheduleDTO>, ICreatePMScheduleValidation
{
public CreatePMScheduleValidation()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("PM Schedule name is required")
.MaximumLength(200).WithMessage("Name cannot exceed 200 characters");
RuleFor(x => x.Frequency)
.NotEmpty().WithMessage("Frequency is required");
RuleFor(x => x.StartDate)
.NotEmpty().WithMessage("Start date is required")
.LessThanOrEqualTo(DateTime.Now.AddYears(1))
.WithMessage("Start date cannot be more than 1 year in the future");
RuleFor(x => x.AssetId)
.GreaterThan(0).WithMessage("Valid asset must be selected")
.When(x => x.AssetId.HasValue);
RuleFor(x => x.LocationId)
.GreaterThan(0).WithMessage("Valid location must be selected")
.When(x => x.LocationId.HasValue);
}
}
public interface IUpdatePMScheduleValidation : IValidator<UpdatePMScheduleDTO>
{
}
public class UpdatePMScheduleValidation : AbstractValidator<UpdatePMScheduleDTO>, IUpdatePMScheduleValidation
{
public UpdatePMScheduleValidation()
{
RuleFor(x => x.Name)
.MaximumLength(200).WithMessage("Name cannot exceed 200 characters")
.When(x => !string.IsNullOrEmpty(x.Name));
RuleFor(x => x.StartDate)
.LessThanOrEqualTo(DateTime.Now.AddYears(1))
.When(x => x.StartDate.HasValue)
.WithMessage("Start date cannot be more than 1 year in the future");
RuleFor(x => x.AssetId)
.GreaterThan(0).WithMessage("Valid asset must be selected")
.When(x => x.AssetId.HasValue);
RuleFor(x => x.LocationId)
.GreaterThan(0).WithMessage("Valid location must be selected")
.When(x => x.LocationId.HasValue);
}
}
}

View file

@ -0,0 +1,81 @@
using FluentValidation;
using SeaHaven.Services.DTOs;
namespace SeaHaven.Services.Validation
{
public interface ICreateVendorValidation : IValidator<CreateVendorDTO>
{
}
public class CreateVendorValidation : AbstractValidator<CreateVendorDTO>, ICreateVendorValidation
{
public CreateVendorValidation()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Vendor name is required")
.MaximumLength(200).WithMessage("Vendor name cannot exceed 200 characters");
RuleFor(x => x.Email)
.EmailAddress().WithMessage("Invalid email address")
.When(x => !string.IsNullOrEmpty(x.Email));
RuleFor(x => x.Phone)
.MaximumLength(20).WithMessage("Phone number cannot exceed 20 characters")
.When(x => !string.IsNullOrEmpty(x.Phone));
RuleFor(x => x.Address)
.MaximumLength(500).WithMessage("Address cannot exceed 500 characters")
.When(x => !string.IsNullOrEmpty(x.Address));
RuleFor(x => x.City)
.MaximumLength(100).WithMessage("City cannot exceed 100 characters")
.When(x => !string.IsNullOrEmpty(x.City));
RuleFor(x => x.State)
.MaximumLength(2).WithMessage("State must be 2 characters")
.When(x => !string.IsNullOrEmpty(x.State));
RuleFor(x => x.Zipcode)
.MaximumLength(10).WithMessage("Zipcode cannot exceed 10 characters")
.When(x => !string.IsNullOrEmpty(x.Zipcode));
}
}
public interface IUpdateVendorValidation : IValidator<UpdateVendorDTO>
{
}
public class UpdateVendorValidation : AbstractValidator<UpdateVendorDTO>, IUpdateVendorValidation
{
public UpdateVendorValidation()
{
RuleFor(x => x.Name)
.MaximumLength(200).WithMessage("Vendor name cannot exceed 200 characters")
.When(x => !string.IsNullOrEmpty(x.Name));
RuleFor(x => x.Email)
.EmailAddress().WithMessage("Invalid email address")
.When(x => !string.IsNullOrEmpty(x.Email));
RuleFor(x => x.Phone)
.MaximumLength(20).WithMessage("Phone number cannot exceed 20 characters")
.When(x => !string.IsNullOrEmpty(x.Phone));
RuleFor(x => x.Address)
.MaximumLength(500).WithMessage("Address cannot exceed 500 characters")
.When(x => !string.IsNullOrEmpty(x.Address));
RuleFor(x => x.City)
.MaximumLength(100).WithMessage("City cannot exceed 100 characters")
.When(x => !string.IsNullOrEmpty(x.City));
RuleFor(x => x.State)
.MaximumLength(2).WithMessage("State must be 2 characters")
.When(x => !string.IsNullOrEmpty(x.State));
RuleFor(x => x.Zipcode)
.MaximumLength(10).WithMessage("Zipcode cannot exceed 10 characters")
.When(x => !string.IsNullOrEmpty(x.Zipcode));
}
}
}

View file

@ -0,0 +1,71 @@
using FluentValidation;
using SeaHaven.Services.DTOs;
using Data.SeaHavenIndustries.Enums;
namespace SeaHaven.Services.Validation
{
public interface ICreateWorkOrderValidation : IValidator<CreateWorkOrderDTO>
{
}
public class CreateWorkOrderValidation : AbstractValidator<CreateWorkOrderDTO>, ICreateWorkOrderValidation
{
public CreateWorkOrderValidation()
{
RuleFor(x => x.WorkerOrderTitle)
.NotEmpty().WithMessage("Work order title is required")
.MaximumLength(200).WithMessage("Title cannot exceed 200 characters");
RuleFor(x => x.Description)
.MaximumLength(2000).WithMessage("Description cannot exceed 2000 characters")
.When(x => !string.IsNullOrEmpty(x.Description));
RuleFor(x => x.LocationId)
.GreaterThan(0).WithMessage("Valid location must be selected")
.When(x => x.LocationId.HasValue);
RuleFor(x => x.Priority)
.IsInEnum().WithMessage("Invalid priority value")
.When(x => x.Priority.HasValue);
RuleFor(x => x.Status)
.IsInEnum().WithMessage("Invalid status value")
.When(x => x.Status.HasValue);
RuleFor(x => x.DueDate)
.GreaterThan(DateTime.Now.AddDays(-1))
.When(x => x.DueDate.HasValue)
.WithMessage("Due date cannot be in the past");
}
}
public interface IUpdateWorkOrderValidation : IValidator<UpdateWorkOrderDTO>
{
}
public class UpdateWorkOrderValidation : AbstractValidator<UpdateWorkOrderDTO>, IUpdateWorkOrderValidation
{
public UpdateWorkOrderValidation()
{
RuleFor(x => x.WorkerOrderTitle)
.MaximumLength(200).WithMessage("Title cannot exceed 200 characters")
.When(x => !string.IsNullOrEmpty(x.WorkerOrderTitle));
RuleFor(x => x.Description)
.MaximumLength(2000).WithMessage("Description cannot exceed 2000 characters")
.When(x => !string.IsNullOrEmpty(x.Description));
RuleFor(x => x.LocationId)
.GreaterThan(0).WithMessage("Valid location must be selected")
.When(x => x.LocationId.HasValue);
RuleFor(x => x.Priority)
.IsInEnum().WithMessage("Invalid priority value")
.When(x => x.Priority.HasValue);
RuleFor(x => x.Status)
.IsInEnum().WithMessage("Invalid status value")
.When(x => x.Status.HasValue);
}
}
}