backend changes

This commit is contained in:
npalOmega 2026-05-14 11:00:12 -05:00
parent 5e4d9894e9
commit 59385cf5b1
54 changed files with 4134 additions and 731 deletions

344
ARCHITECTURE_AT_A_GLANCE.md Normal file
View file

@ -0,0 +1,344 @@
# Architecture At A Glance
## 📐 The 4-Layer Architecture
```
┌──────────────────────────────────────────────────────┐
│ 🌐 API LAYER (Controllers) │
│ - HTTP Endpoints │
│ - Authentication │
│ - Exception → HTTP Status Code mapping │
│ ❌ NO Business Logic │
│ ❌ NO Database Access │
└─────────────────┬────────────────────────────────────┘
│
│ Calls
▼
┌──────────────────────────────────────────────────────┐
│ 💼 SERVICE LAYER (Business Logic) │
│ - FluentValidation │
│ - Business Rules │
│ - Orchestration │
│ - DTO Transformations │
│ ❌ NO HTTP Concerns │
│ ❌ NO Database Access │
└─────────────────┬────────────────────────────────────┘
│
│ Calls
▼
┌──────────────────────────────────────────────────────┐
│ 💾 DATA SERVICE LAYER (Persistence) │
│ - CRUD Operations │
│ - Database Queries │
│ - Audit Field Management │
│ ❌ NO Validation │
│ ❌ NO Business Logic │
└─────────────────┬────────────────────────────────────┘
│
│ Uses
▼
┌──────────────────────────────────────────────────────┐
│ 🗄️ DATA LAYER (EF Core + Entities) │
│ - DbContext │
│ - Entity Models (POCOs) │
│ - Migrations │
└──────────────────────────────────────────────────────┘
```
---
## 🎯 Responsibilities Matrix
| Layer | What It Does | What It Doesn't Do |
|-------|--------------|-------------------|
| **Controller** | • Handle HTTP requests<br>• Authenticate users<br>• Map API DTOs<br>• Catch exceptions<br>• Return HTTP status codes | • Business logic<br>• Validation rules<br>• Database queries |
| **Service** | • Validate data (FluentValidation)<br>• Apply business rules<br>• Coordinate operations<br>• Transform data | • HTTP concerns<br>• Database operations |
| **DataService** | • Execute database queries<br>• CRUD operations<br>• Set audit fields | • Validation<br>• Business rules |
| **Data** | • Define entities<br>• Database schema | • Application logic |
---
## 🔄 Request Flow
### Creating a Contact (Example)
```
1. HTTP POST /api/Contact/Create
↓
2. ContactController.Create()
- Checks authentication
- Gets userId from claims
↓
3. ContactService.CreateContactAsync()
- Validates using FluentValidation
- Applies business rules
↓
4. ContactDataService.CreateAsync()
- Creates entity
- Sets audit fields
- Saves to database
↓
5. Returns through layers:
Entity → DataDTO → ServiceDTO → ApiDTO → HTTP Response
```
---
## 📦 DTO Flow
```
Frontend (JSON)
↓
API DTO (Contact_DTO) ← Controller receives this
↓
Service DTO (CreateContactDTO) ← Service validates this
↓
Data DTO (CreateContactDataDTO) ← DataService uses this
↓
Entity (Contact) ← Database stores this
```
---
## 🔌 Dependency Injection
### Automatic Assembly Scanning
Services auto-register via assembly scanning (no manual registration needed).
```csharp
// Program.cs
builder.Services.AddDataServices(); // Auto-register data services
builder.Services.AddBusinessServices(); // Auto-register business services + validators
```
### Naming Convention (REQUIRED)
| Interface | Implementation | Registered? |
|-----------|---------------|-------------|
| `IContactService` | `ContactService` | ✅ YES |
| `ISomething` | `SomethingElse` | ❌ NO |
**Rule**: Interface = `I` + Class name exactly
---
## ✅ Validation
### Where and How
```
❌ Controller → Catches ValidationException
✅ Service → Runs FluentValidation.Validate()
❌ DataService → No validation
```
### FluentValidation Example
```csharp
public class CreateContactDTOValidator : AbstractValidator<CreateContactDTO>
{
public CreateContactDTOValidator()
{
RuleFor(x => x.FirstName)
.NotEmpty().WithMessage("First name is required")
.MaximumLength(50);
RuleFor(x => x.Email)
.EmailAddress().WithMessage("Invalid email");
}
}
```
### In Service
```csharp
var validationResult = await _validator.ValidateAsync(dto);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
```
---
## 🚨 Exception Handling
### Standard Pattern (All Controllers)
```csharp
try
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return Unauthorized(...);
await _service.SomeMethod(dto, userId);
return Ok(...);
}
catch (ValidationException vex) // 400 Bad Request
{
return BadRequest(...);
}
catch (KeyNotFoundException) // 404 Not Found
{
return NotFound(...);
}
catch (Exception ex) // 500 Internal Server Error
{
return StatusCode(500, ...);
}
```
---
## 🔐 Authentication
### Getting User ID
```csharp
// Method 1: Claims (Preferred)
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
// Method 2: UserManager (for Identity)
var userId = _userManager.GetUserId(User);
// Always check for null
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
```
### Audit Fields
Every entity has:
```csharp
public string CreatedBy { get; set; } // User ID (GUID)
public DateTime CreatedDate { get; set; } // UTC timestamp
public string ModifiedBy { get; set; } // User ID (GUID)
public DateTime ModifiedDate { get; set; } // UTC timestamp
```
Set in DataService:
```csharp
CreatedBy = userId,
CreatedDate = DateTime.UtcNow,
ModifiedBy = userId,
ModifiedDate = DateTime.UtcNow
```
---
## 📋 Naming Conventions
| Component | Pattern | Example |
|-----------|---------|---------|
| Entity | `[Entity]` | `Contact` |
| Controller | `[Entity]Controller` | `ContactController` |
| Service Interface | `I[Entity]Service` | `IContactService` |
| Service Class | `[Entity]Service` | `ContactService` |
| DataService Interface | `I[Entity]DataService` | `IContactDataService` |
| DataService Class | `[Entity]DataService` | `ContactDataService` |
| API DTO | `[Entity]_DTO` | `Contact_DTO` |
| Service DTO | `[Action][Entity]DTO` | `CreateContactDTO` |
| Data DTO | `[Action][Entity]DataDTO` | `CreateContactDataDTO` |
| Validator | `[DTO]Validator` | `CreateContactDTOValidator` |
---
## 🎨 Code Templates
### Controller Method Template
```csharp
[HttpPost("ActionName")]
public async Task<IActionResult> ActionName([FromBody] SomeDTO dto)
{
try
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
await _service.Method(dto, userId);
return Ok(new Response { Status = "Success", Message = "Success message" });
}
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 = "Not found" });
}
catch (Exception ex)
{
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
```
### Service Method Template
```csharp
public async Task<ResultDTO> MethodAsync(InputDTO dto, string userId)
{
// 1. Validate
var validationResult = await _validator.ValidateAsync(dto);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
// 2. Business logic (if any)
// 3. Call DataService
var result = await _dataService.MethodAsync(dataDto, userId);
// 4. Map and return
return MapToServiceDTO(result);
}
```
### DataService Method Template
```csharp
public async Task<DataDTO> MethodAsync(InputDataDTO dto, string userId)
{
// 1. Create/modify entity
var entity = new Entity
{
Property = dto.Property,
CreatedBy = userId,
CreatedDate = DateTime.UtcNow,
ModifiedBy = userId,
ModifiedDate = DateTime.UtcNow
};
// 2. Database operation
_db.Entities.Add(entity);
await _db.SaveChangesAsync();
// 3. Map and return
return MapToDataDTO(entity);
}
```
---
## 📚 Key Rules
1. **Never skip layers** - Always go Controller → Service → DataService
2. **No direct `_db` in Controllers** - Database access only in DataService
3. **Validate in Service** - Use FluentValidation, not manual checks
4. **Always check userId** - Return 401 if null
5. **Catch ValidationException first** - Before generic Exception
6. **Use interfaces everywhere** - For dependency injection
7. **Set audit fields** - CreatedBy, CreatedDate, ModifiedBy, ModifiedDate
8. **Return proper HTTP codes** - 200, 400, 401, 404, 500
---
## 🎓 Learn More
- **Full Details**: See `BACKEND_ARCHITECTURE.md`
- **Step-by-Step Guide**: See `QUICK_START_GUIDE.md`
- **Working Examples**: Look at `ContactController`, `ContactService`, `ContactDataService`

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

@ -0,0 +1,237 @@
# Dependency Injection - Assembly Scanning Guide
## 🎯 Overview
This project uses **assembly scanning** to automatically register services instead of manual registration. This means you don't need to add each service to `Program.cs` individually.
---
## 🔍 How It Works
### In `Program.cs`
```csharp
// These two lines register ALL services automatically
builder.Services.AddDataServices(); // Scans SeaHaven.DataServices assembly
builder.Services.AddBusinessServices(); // Scans SeaHaven.Services assembly + Validators
```
That's it! No need for this:
```csharp
// ❌ DON'T DO THIS - It's automatic now!
builder.Services.AddScoped<IContactDataService, ContactDataService>();
builder.Services.AddScoped<IEmployeeDataService, EmployeeDataService>();
builder.Services.AddScoped<IAssetDataService, AssetDataService>();
// ... hundreds of lines
```
---
## 📦 DataServicesModule
**Location**: `SeaHaven.DataServices/DependencyInjection/DataServicesModule.cs`
```csharp
public static IServiceCollection AddDataServices(this IServiceCollection services)
{
var assembly = Assembly.GetExecutingAssembly();
// Find all non-abstract classes in the assembly
var allClasses = assembly.GetTypes()
.Where(t => t.IsClass && !t.IsAbstract && !t.IsGenericType)
.ToList();
foreach (var implementationType in allClasses)
{
// Find interface matching pattern: I{ClassName}
var defaultInterface = implementationType.GetInterfaces()
.FirstOrDefault(i => i.Name == $"I{implementationType.Name}");
if (defaultInterface != null)
{
// Register as Scoped
services.AddScoped(defaultInterface, implementationType);
}
}
return services;
}
```
### What It Does
1. Scans entire `SeaHaven.DataServices` assembly
2. Finds all concrete classes (e.g., `ContactDataService`, `EmployeeDataService`)
3. For each class, looks for an interface named `I{ClassName}`
4. If found, registers the pair as **Scoped** service
### Example
If you create:
- `IProductDataService` (interface)
- `ProductDataService` (class implementing `IProductDataService`)
Then `IProductDataService → ProductDataService` is **automatically registered** as Scoped.
---
## 💼 ServicesModule
**Location**: `SeaHaven.Services/DependencyInjection/ServicesModule.cs`
```csharp
public static IServiceCollection AddBusinessServices(this IServiceCollection services)
{
var assembly = Assembly.GetExecutingAssembly();
var allClasses = assembly.GetTypes()
.Where(t => t.IsClass && !t.IsAbstract && !t.IsGenericType)
.ToList();
foreach (var implementationType in allClasses)
{
var defaultInterface = implementationType.GetInterfaces()
.FirstOrDefault(i => i.Name == $"I{implementationType.Name}");
if (defaultInterface != null)
{
services.AddScoped(defaultInterface, implementationType);
}
}
// ALSO auto-register all FluentValidation validators
services.AddValidatorsFromAssembly(assembly);
return services;
}
```
### What It Does
1. Scans entire `SeaHaven.Services` assembly
2. Registers all services using naming convention (same as DataServices)
3. **BONUS**: Automatically registers ALL FluentValidation validators in the assembly
### Example
If you create:
- `IProductService` (interface)
- `ProductService` (class)
- `CreateProductDTOValidator` (FluentValidation validator)
Then **all three** are automatically registered.
---
## ⚠️ CRITICAL: Naming Convention
For auto-registration to work, you MUST follow this pattern:
### ✅ CORRECT
| Interface | Implementation | Result |
|-----------|---------------|--------|
| `IContactService` | `ContactService` | ✅ Registered as Scoped |
| `IContactDataService` | `ContactDataService` | ✅ Registered as Scoped |
| `IProductService` | `ProductService` | ✅ Registered as Scoped |
| `IEmployeeService` | `EmployeeService` | ✅ Registered as Scoped |
### ❌ WRONG
| Interface | Implementation | Result |
|-----------|---------------|--------|
| `IProductService` | `ProductServiceImpl` | ❌ NOT registered - names don't match |
| `IContactRepository` | `ContactService` | ❌ NOT registered - names don't match |
| `ISomething` | `SomethingElse` | ❌ NOT registered - names don't match |
---
## 📋 Rules
1. **Interface naming**: `I{ClassName}`
2. **Class naming**: `{ClassName}`
3. **Example**:
- Interface: `IContactService`
- Class: `ContactService` (NOT `ContactServiceImpl`, `ContactServiceImplementation`, etc.)
---
## 🚀 Benefits
✅ **Less boilerplate** - No need to add each service to `Program.cs`
✅ **Auto-discovery** - New services are automatically registered
✅ **Consistent** - All services registered the same way
✅ **FluentValidation included** - Validators work automatically
✅ **Cleaner Program.cs** - Only 2 lines instead of 50+
---
## 🎯 How to Add a New Service
### Old Way (Manual)
```csharp
// 1. Create interface and class
public interface IProductService { }
public class ProductService : IProductService { }
// 2. Manually add to Program.cs
builder.Services.AddScoped<IProductService, ProductService>(); // ❌ DON'T DO THIS
```
### New Way (Automatic)
```csharp
// 1. Create interface and class following naming convention
public interface IProductService { }
public class ProductService : IProductService { }
// 2. Done! It's automatically registered ✅
```
---
## 🔧 Lifecycle
All services are registered as **Scoped**:
- New instance per HTTP request
- Shared within the same request
- Disposed at end of request
---
## 📚 Example Flow
1. You create `IProductDataService` and `ProductDataService` in `SeaHaven.DataServices`
2. You create `IProductService` and `ProductService` in `SeaHaven.Services`
3. You create `CreateProductDTOValidator` in `SeaHaven.Services/Validation`
4. Application starts → `AddDataServices()` and `AddBusinessServices()` run
5. Assembly scanner finds your services and validator
6. All registered automatically as Scoped
7. You inject `IProductService` in controller → works immediately ✅
---
## 🐛 Troubleshooting
### Service not being injected?
Check:
1. ✅ Is the interface named `I{ClassName}`?
2. ✅ Is the class named `{ClassName}`?
3. ✅ Is the class in the correct assembly?
4. ✅ Is the class public and concrete (not abstract)?
5. ✅ Does the class implement the interface?
### Validator not working?
Check:
1. ✅ Is the validator in `SeaHaven.Services` assembly?
2. ✅ Does it inherit from `AbstractValidator<T>`?
3. ✅ Is it public and concrete?
---
## 📖 See Also
- **Full Architecture Guide**: `BACKEND_ARCHITECTURE.md`
- **Quick Start Guide**: `QUICK_START_GUIDE.md`
- **At a Glance**: `ARCHITECTURE_AT_A_GLANCE.md`

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

515
QUICK_START_GUIDE.md Normal file
View file

@ -0,0 +1,515 @@
# Backend Quick Start Guide
## 🚀 Adding a New Feature (Step-by-Step)
### Example: Adding a "Product" Module
---
## Step 1: Create Entity (Data Layer)
**File**: `Data.SeaHavenIndustries/Models/Product.cs`
```csharp
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public decimal Price { get; set; }
public bool IsActive { get; set; }
// Audit fields (REQUIRED)
public string CreatedBy { get; set; } = string.Empty;
public DateTime CreatedDate { get; set; }
public string ModifiedBy { get; set; } = string.Empty;
public DateTime ModifiedDate { get; set; }
}
```
**Add to DbContext**: `Data.SeaHavenIndustries/ApplicationDbContext.cs`
```csharp
public DbSet<Product> Products { get; set; }
```
---
## Step 2: Create Migration
```bash
cd backend/shoc-backend
dotnet ef migrations add AddProductTable --project Data.SeaHavenIndustries --startup-project Api.SeaHavenIndustries
dotnet ef database update --project Data.SeaHavenIndustries --startup-project Api.SeaHavenIndustries
```
---
## Step 3: Create DTOs
### 3A. Data Layer DTOs (`SeaHaven.DataServices/DTOs/`)
**CreateProductDataDTO.cs**
```csharp
public class CreateProductDataDTO
{
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public decimal Price { get; set; }
}
```
**ProductDataDTO.cs**
```csharp
public class ProductDataDTO
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public decimal Price { get; set; }
public bool IsActive { get; set; }
}
```
### 3B. Service Layer DTOs (`SeaHaven.Services/DTOs/`)
**CreateProductDTO.cs**
```csharp
public class CreateProductDTO
{
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public decimal Price { get; set; }
}
```
**ProductDTO.cs**
```csharp
public class ProductDTO
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public decimal Price { get; set; }
public bool IsActive { get; set; }
}
```
### 3C. API Layer DTOs (`Api.SeaHavenIndustries/DTOs/`)
**Product_DTO.cs**
```csharp
public class Product_DTO
{
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public decimal Price { get; set; }
}
```
---
## Step 4: Create FluentValidation Validator
**File**: `SeaHaven.Services/Validators/CreateProductDTOValidator.cs`
```csharp
using FluentValidation;
using SeaHaven.Services.DTOs;
public class CreateProductDTOValidator : AbstractValidator<CreateProductDTO>
{
public CreateProductDTOValidator()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Product name is required")
.MaximumLength(100).WithMessage("Name cannot exceed 100 characters");
RuleFor(x => x.Price)
.GreaterThan(0).WithMessage("Price must be greater than 0");
}
}
```
---
## Step 5: Create DataService
### 5A. Interface (`SeaHaven.DataServices/Interfaces/IProductDataService.cs`)
```csharp
public interface IProductDataService
{
Task<ProductDataDTO> CreateAsync(CreateProductDataDTO dto, string userId);
Task<ProductDataDTO> UpdateAsync(int id, UpdateProductDataDTO dto, string userId);
Task DeleteAsync(int id, string userId);
Task<ProductDataDTO?> GetByIdAsync(int id);
Task<PagedResult<ProductDataDTO>> GetPagedAsync(int page, int pageSize, string? search);
}
```
### 5B. Implementation (`SeaHaven.DataServices/Implementation/ProductDataService.cs`)
```csharp
using Data.SeaHavenIndustries;
using Data.SeaHavenIndustries.Models;
using Microsoft.EntityFrameworkCore;
public class ProductDataService : IProductDataService
{
private readonly ApplicationDbContext _db;
public ProductDataService(ApplicationDbContext db)
{
_db = db;
}
public async Task<ProductDataDTO> CreateAsync(CreateProductDataDTO dto, string userId)
{
var product = new Product
{
Name = dto.Name,
Description = dto.Description,
Price = dto.Price,
IsActive = true,
CreatedBy = userId,
CreatedDate = DateTime.UtcNow,
ModifiedBy = userId,
ModifiedDate = DateTime.UtcNow
};
_db.Products.Add(product);
await _db.SaveChangesAsync();
return new ProductDataDTO
{
Id = product.Id,
Name = product.Name,
Description = product.Description,
Price = product.Price,
IsActive = product.IsActive
};
}
public async Task<ProductDataDTO?> GetByIdAsync(int id)
{
var product = await _db.Products.FindAsync(id);
if (product == null) return null;
return new ProductDataDTO
{
Id = product.Id,
Name = product.Name,
Description = product.Description,
Price = product.Price,
IsActive = product.IsActive
};
}
// Implement other methods...
}
```
---
## Step 6: Create Service
### 6A. Interface (`SeaHaven.Services/Interfaces/IProductService.cs`)
```csharp
public interface IProductService
{
Task<ProductDTO> CreateProductAsync(CreateProductDTO dto, string userId);
Task<ProductDTO> UpdateProductAsync(int id, UpdateProductDTO dto, string userId);
Task DeleteProductAsync(int id, string userId);
Task<ProductDTO> GetProductByIdAsync(int id);
Task<PagedResult<ProductDTO>> GetProductsPagedAsync(int page, int pageSize, string? search);
}
```
### 6B. Implementation (`SeaHaven.Services/Implementation/ProductService.cs`)
```csharp
using FluentValidation;
using SeaHaven.DataServices.Interfaces;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Interfaces;
public class ProductService : IProductService
{
private readonly IProductDataService _dataService;
private readonly IValidator<CreateProductDTO> _createValidator;
public ProductService(
IProductDataService dataService,
IValidator<CreateProductDTO> createValidator)
{
_dataService = dataService;
_createValidator = createValidator;
}
public async Task<ProductDTO> CreateProductAsync(CreateProductDTO dto, string userId)
{
// Validate
var validationResult = await _createValidator.ValidateAsync(dto);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
// Map to Data DTO
var dataDto = new CreateProductDataDTO
{
Name = dto.Name,
Description = dto.Description,
Price = dto.Price
};
// Call DataService
var created = await _dataService.CreateAsync(dataDto, userId);
// Map to Service DTO
return new ProductDTO
{
Id = created.Id,
Name = created.Name,
Description = created.Description,
Price = created.Price,
IsActive = created.IsActive
};
}
public async Task<ProductDTO> GetProductByIdAsync(int id)
{
var product = await _dataService.GetByIdAsync(id);
if (product == null)
throw new KeyNotFoundException($"Product with ID {id} not found");
return new ProductDTO
{
Id = product.Id,
Name = product.Name,
Description = product.Description,
Price = product.Price,
IsActive = product.IsActive
};
}
// Implement other methods...
}
```
---
## Step 7: Create Controller
**File**: `Api.SeaHavenIndustries/Controllers/ProductController.cs`
```csharp
using Api.SeaHavenIndustries.DTOs;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using SeaHaven.Services.Interfaces;
using System.Security.Claims;
[ApiController]
[Route("api/[controller]")]
public class ProductController : Controller
{
private readonly IProductService _productService;
public ProductController(IProductService productService)
{
_productService = productService;
}
[HttpPost("Create")]
public async Task<IActionResult> Create([FromBody] Product_DTO dto)
{
try
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
var serviceDto = new CreateProductDTO
{
Name = dto.Name,
Description = dto.Description,
Price = dto.Price
};
await _productService.CreateProductAsync(serviceDto, userId);
return Ok(new Response { Status = "Success", Message = "Product created 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 StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
[HttpGet("GetById")]
public async Task<IActionResult> GetById(int id)
{
try
{
var product = await _productService.GetProductByIdAsync(id);
return Ok(product);
}
catch (KeyNotFoundException)
{
return NotFound(new Response { Status = "Error", Message = "Product not found" });
}
catch (Exception ex)
{
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
// Add other endpoints...
}
```
---
## Step 8: Services Auto-Registered ✅
**No action needed!** Services are automatically registered by assembly scanning.
If you followed naming conventions (`IProductService` → `ProductService`), your services are already registered.
**How?** These lines in `Program.cs` scan assemblies:
```csharp
builder.Services.AddDataServices(); // Scans SeaHaven.DataServices
builder.Services.AddBusinessServices(); // Scans SeaHaven.Services + Validators
```
---
## Step 9: Test
```bash
# Build
dotnet build
# Run
dotnet run --project Api.SeaHavenIndustries
# Test endpoint
curl -X POST "http://localhost:5141/api/Product/Create" \
-H "Content-Type: application/json" \
-d '{"name":"Test Product","description":"Test","price":99.99}'
```
---
## ✅ Checklist
- [ ] Entity created with audit fields
- [ ] DbSet added to DbContext
- [ ] Migration created and applied
- [ ] Data DTOs created
- [ ] Service DTOs created
- [ ] API DTOs created
- [ ] FluentValidation validator created
- [ ] DataService interface created
- [ ] DataService implementation created
- [ ] Service interface created
- [ ] Service implementation created
- [ ] Controller created
- [ ] Services registered in DI container
- [ ] Exception handling implemented
- [ ] User ID authentication added
- [ ] Build succeeds
- [ ] Endpoints tested
---
## 🎯 Common Patterns
### Get List with Pagination
```csharp
[HttpGet("GetList")]
public async Task<IActionResult> GetList(string? search = "", int page = 1, int pageSize = 10)
{
var result = await _productService.GetProductsPagedAsync(page, pageSize, search);
return Ok(new Pagination_DTO
{
Data = result.Items,
PageNumber = page,
PageSize = pageSize,
TotalCount = result.TotalCount,
TotalPages = (int)Math.Ceiling(result.TotalCount / (double)pageSize)
});
}
```
### Update
```csharp
[HttpPost("Update")]
public async Task<IActionResult> Update([FromBody] EditProduct_DTO dto)
{
try
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
await _productService.UpdateProductAsync(dto.Id, dto.ToServiceDTO(), userId);
return Ok(new Response { Status = "Success", Message = "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 = "Not found" });
}
catch (Exception ex)
{
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
```
### Delete
```csharp
[HttpPost("Delete")]
public async Task<IActionResult> Delete(int id)
{
try
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
await _productService.DeleteProductAsync(id, userId);
return Ok(new Response { Status = "Success", Message = "Deleted" });
}
catch (KeyNotFoundException)
{
return NotFound(new Response { Status = "Error", Message = "Not found" });
}
catch (Exception ex)
{
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
```
---
## 📚 Reference Implementations
For complete working examples, see:
- `ContactController`, `ContactService`, `ContactDataService`
- `EmployeeController`, `EmployeeService`, `EmployeeDataService`
- `AssetController`, `AssetService`, `AssetDataService`

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