From 59385cf5b1171b9ec226c2f17b35f931473ef101 Mon Sep 17 00:00:00 2001 From: npalOmega Date: Thu, 14 May 2026 11:00:12 -0500 Subject: [PATCH] backend changes --- ARCHITECTURE_AT_A_GLANCE.md | 344 +++++++++ .../Controllers/AccountController.cs | 53 +- .../Controllers/AssetController.cs | 43 +- .../Controllers/ContactController.cs | 66 +- .../Controllers/EmployeeController.cs | 103 ++- .../Controllers/FollowUpController.cs | 108 ++- .../Controllers/LocationController.cs | 32 +- .../Controllers/PMScheduleController.cs | 71 +- .../Controllers/VendorController.cs | 77 +- .../Controllers/WorkOrderController.cs | 713 +++++++----------- Api.SeaHavenIndustries/DTOs/Workorder_DTO.cs | 83 +- Api.SeaHavenIndustries/Program.cs | 36 +- BACKEND_ARCHITECTURE.md | 531 +++++++++++++ DI_ASSEMBLY_SCANNING_GUIDE.md | 237 ++++++ .../Enums/WorkOrderEnums.cs | 92 +++ QUICK_START_GUIDE.md | 515 +++++++++++++ .../DependencyInjection/DataServicesModule.cs | 35 + .../Implementation/CommentDataService.cs | 75 ++ .../Implementation/DispatchDataService.cs | 105 +++ .../Implementation/QuotesDataService.cs | 75 ++ .../TaskListTemplateDataService.cs | 61 ++ .../Implementation/UserDataService.cs | 37 + .../Implementation/WorkOrderDataService.cs | 93 +++ .../Interfaces/ICommentDataService.cs | 16 + .../Interfaces/IDispatchDataService.cs | 12 + .../Interfaces/IQuotesDataService.cs | 16 + .../ITaskListTemplateDataService.cs | 15 + .../Interfaces/IUserDataService.cs | 12 + .../Interfaces/IWorkOrderDataService.cs | 21 +- SeaHaven.Services/DTOs/ContactDTOs.cs | 12 - SeaHaven.Services/DTOs/WorkOrderDTOs.cs | 97 ++- .../DependencyInjection/ServicesModule.cs | 38 + .../Implementation/AccountService.cs | 23 +- .../Implementation/AssetService.cs | 23 +- .../Implementation/ContactService.cs | 35 +- .../Implementation/DispatchService.cs | 23 +- .../Implementation/EmployeeService.cs | 23 +- .../Implementation/FollowUpService.cs | 23 +- .../Implementation/LocationService.cs | 23 +- .../Implementation/PMScheduleService.cs | 23 +- .../Implementation/VendorService.cs | 23 +- .../Implementation/WorkOrderService.cs | 150 ++-- .../Interfaces/IWorkOrderService.cs | 6 +- SeaHaven.Services/SeaHaven.Services.csproj | 5 + .../Validation/AccountValidation.cs | 57 ++ .../Validation/AssetValidation.cs | 41 + .../Validation/ContactValidation.cs | 73 ++ .../Validation/DispatchValidation.cs | 44 ++ .../Validation/EmployeeValidation.cs | 81 ++ .../Validation/FollowUpValidation.cs | 78 ++ .../Validation/LocationValidation.cs | 73 ++ .../Validation/PMScheduleValidation.cs | 62 ++ .../Validation/VendorValidation.cs | 81 ++ .../Validation/WorkOrderValidation.cs | 71 ++ 54 files changed, 4134 insertions(+), 731 deletions(-) create mode 100644 ARCHITECTURE_AT_A_GLANCE.md create mode 100644 BACKEND_ARCHITECTURE.md create mode 100644 DI_ASSEMBLY_SCANNING_GUIDE.md create mode 100644 Data.SeaHavenIndustries/Enums/WorkOrderEnums.cs create mode 100644 QUICK_START_GUIDE.md create mode 100644 SeaHaven.DataServices/DependencyInjection/DataServicesModule.cs create mode 100644 SeaHaven.DataServices/Implementation/CommentDataService.cs create mode 100644 SeaHaven.DataServices/Implementation/QuotesDataService.cs create mode 100644 SeaHaven.DataServices/Implementation/TaskListTemplateDataService.cs create mode 100644 SeaHaven.DataServices/Implementation/UserDataService.cs create mode 100644 SeaHaven.DataServices/Interfaces/ICommentDataService.cs create mode 100644 SeaHaven.DataServices/Interfaces/IQuotesDataService.cs create mode 100644 SeaHaven.DataServices/Interfaces/ITaskListTemplateDataService.cs create mode 100644 SeaHaven.DataServices/Interfaces/IUserDataService.cs create mode 100644 SeaHaven.Services/DependencyInjection/ServicesModule.cs create mode 100644 SeaHaven.Services/Validation/AccountValidation.cs create mode 100644 SeaHaven.Services/Validation/AssetValidation.cs create mode 100644 SeaHaven.Services/Validation/ContactValidation.cs create mode 100644 SeaHaven.Services/Validation/DispatchValidation.cs create mode 100644 SeaHaven.Services/Validation/EmployeeValidation.cs create mode 100644 SeaHaven.Services/Validation/FollowUpValidation.cs create mode 100644 SeaHaven.Services/Validation/LocationValidation.cs create mode 100644 SeaHaven.Services/Validation/PMScheduleValidation.cs create mode 100644 SeaHaven.Services/Validation/VendorValidation.cs create mode 100644 SeaHaven.Services/Validation/WorkOrderValidation.cs diff --git a/ARCHITECTURE_AT_A_GLANCE.md b/ARCHITECTURE_AT_A_GLANCE.md new file mode 100644 index 0000000..635b2d5 --- /dev/null +++ b/ARCHITECTURE_AT_A_GLANCE.md @@ -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
β€’ Authenticate users
β€’ Map API DTOs
β€’ Catch exceptions
β€’ Return HTTP status codes | β€’ Business logic
β€’ Validation rules
β€’ Database queries | +| **Service** | β€’ Validate data (FluentValidation)
β€’ Apply business rules
β€’ Coordinate operations
β€’ Transform data | β€’ HTTP concerns
β€’ Database operations | +| **DataService** | β€’ Execute database queries
β€’ CRUD operations
β€’ Set audit fields | β€’ Validation
β€’ Business rules | +| **Data** | β€’ Define entities
β€’ 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 +{ + 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 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 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 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` diff --git a/Api.SeaHavenIndustries/Controllers/AccountController.cs b/Api.SeaHavenIndustries/Controllers/AccountController.cs index 3e5c879..72b3a8c 100644 --- a/Api.SeaHavenIndustries/Controllers/AccountController.cs +++ b/Api.SeaHavenIndustries/Controllers/AccountController.cs @@ -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 }); } } diff --git a/Api.SeaHavenIndustries/Controllers/AssetController.cs b/Api.SeaHavenIndustries/Controllers/AssetController.cs index 88ccb5c..e671ad2 100644 --- a/Api.SeaHavenIndustries/Controllers/AssetController.cs +++ b/Api.SeaHavenIndustries/Controllers/AssetController.cs @@ -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 }); } } diff --git a/Api.SeaHavenIndustries/Controllers/ContactController.cs b/Api.SeaHavenIndustries/Controllers/ContactController.cs index d81153b..be53d37 100644 --- a/Api.SeaHavenIndustries/Controllers/ContactController.cs +++ b/Api.SeaHavenIndustries/Controllers/ContactController.cs @@ -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 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 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 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 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 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 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); diff --git a/Api.SeaHavenIndustries/Controllers/EmployeeController.cs b/Api.SeaHavenIndustries/Controllers/EmployeeController.cs index 64e0bb8..2fa92e1 100644 --- a/Api.SeaHavenIndustries/Controllers/EmployeeController.cs +++ b/Api.SeaHavenIndustries/Controllers/EmployeeController.cs @@ -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 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 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 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 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 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); + } + } diff --git a/Api.SeaHavenIndustries/Controllers/FollowUpController.cs b/Api.SeaHavenIndustries/Controllers/FollowUpController.cs index de65e3e..02bdf0f 100644 --- a/Api.SeaHavenIndustries/Controllers/FollowUpController.cs +++ b/Api.SeaHavenIndustries/Controllers/FollowUpController.cs @@ -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 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 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 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 }); + } } } diff --git a/Api.SeaHavenIndustries/Controllers/LocationController.cs b/Api.SeaHavenIndustries/Controllers/LocationController.cs index 102d206..4bfba79 100644 --- a/Api.SeaHavenIndustries/Controllers/LocationController.cs +++ b/Api.SeaHavenIndustries/Controllers/LocationController.cs @@ -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 }); } } diff --git a/Api.SeaHavenIndustries/Controllers/PMScheduleController.cs b/Api.SeaHavenIndustries/Controllers/PMScheduleController.cs index 900f202..543e687 100644 --- a/Api.SeaHavenIndustries/Controllers/PMScheduleController.cs +++ b/Api.SeaHavenIndustries/Controllers/PMScheduleController.cs @@ -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 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 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 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 }); } } } diff --git a/Api.SeaHavenIndustries/Controllers/VendorController.cs b/Api.SeaHavenIndustries/Controllers/VendorController.cs index 0b26f3e..9f6afc7 100644 --- a/Api.SeaHavenIndustries/Controllers/VendorController.cs +++ b/Api.SeaHavenIndustries/Controllers/VendorController.cs @@ -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 GetVendorById(int id) + [HttpGet("GetById")] + public async Task 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 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 Update(int id, [FromBody] EditVendor_DTO model) + [HttpPost("Update")] + public async Task 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 Delete(int id) + [HttpPost("Delete")] + public async Task 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")] diff --git a/Api.SeaHavenIndustries/Controllers/WorkOrderController.cs b/Api.SeaHavenIndustries/Controllers/WorkOrderController.cs index 946d674..57eb4b8 100644 --- a/Api.SeaHavenIndustries/Controllers/WorkOrderController.cs +++ b/Api.SeaHavenIndustries/Controllers/WorkOrderController.cs @@ -1,5 +1,7 @@ ο»Ώusing Api.SeaHavenIndustries.DTOs; using Data.SeaHavenIndustries; +using Data.SeaHavenIndustries.Enums; +using FluentValidation; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Hosting; @@ -13,6 +15,8 @@ using System.IO; using System.Net.Http.Headers; using System.Security.Claims; using static System.Runtime.InteropServices.JavaScript.JSType; +using SeaHaven.Services.Interfaces; +using SeaHaven.Services.DTOs; using SeaHaven.DataServices.Interfaces; namespace Api.SeaHavenIndustries.Controllers @@ -24,8 +28,15 @@ namespace Api.SeaHavenIndustries.Controllers public class WorkOrderController : Controller { private readonly UserManager _userManager; + private readonly IWorkOrderService _workOrderService; private readonly IWorkOrderDataService _workOrderDataService; - private readonly ApplicationDbContext _db; // Keep for complex operations, file handling, dispatch, comments + private readonly ICommentDataService _commentDataService; + private readonly IQuotesDataService _quotesDataService; + private readonly IUserDataService _userDataService; + private readonly IDispatchDataService _dispatchDataService; + private readonly IVendorDataService _vendorDataService; + private readonly ITaskListTemplateDataService _taskListTemplateDataService; + private readonly ApplicationDbContext _db; private readonly IWebHostEnvironment _webHostEnvironment; IHttpContextAccessor _httpContext; private readonly Helper.SendMessage _sendMessage; @@ -34,7 +45,14 @@ namespace Api.SeaHavenIndustries.Controllers public WorkOrderController( UserManager userManager, + IWorkOrderService workOrderService, IWorkOrderDataService workOrderDataService, + ICommentDataService commentDataService, + IQuotesDataService quotesDataService, + IUserDataService userDataService, + IDispatchDataService dispatchDataService, + IVendorDataService vendorDataService, + ITaskListTemplateDataService taskListTemplateDataService, ApplicationDbContext db, IWebHostEnvironment webHostEnvironment, IHttpContextAccessor httpContext, @@ -43,7 +61,14 @@ namespace Api.SeaHavenIndustries.Controllers IConfiguration config) { _userManager = userManager; + _workOrderService = workOrderService; _workOrderDataService = workOrderDataService; + _commentDataService = commentDataService; + _quotesDataService = quotesDataService; + _userDataService = userDataService; + _dispatchDataService = dispatchDataService; + _vendorDataService = vendorDataService; + _taskListTemplateDataService = taskListTemplateDataService; _db = db; _webHostEnvironment = webHostEnvironment; _httpContext = httpContext; @@ -53,138 +78,87 @@ namespace Api.SeaHavenIndustries.Controllers } [HttpPost] [Route("AddWorkorder")] - public async Task Addworkorder([FromBody] Workorder_DTO model) + public async Task Addworkorder([FromForm] Workorder_DTO model) { try { - string userId = User.FindFirstValue(ClaimTypes.NameIdentifier); - WorkOrder workOrder = new WorkOrder(); - workOrder.InternalWONumber = await GenerateInternalWONumber(); - workOrder.WorkerOrderNumber = model.WorkerOrderNumber; - workOrder.WorkerOrderTitle = model.WorkerOrderTitle; - workOrder.Description = model.Description; - workOrder.TT = model.TT; - workOrder.PO = model.PO; - workOrder.AssignTo = model.AssignTo; - workOrder.DueDate = model.DueDate; - workOrder.Priority = model.Priority; - workOrder.istemplate = model.istemplate; - //workOrder.CategoryId = model.CategoryId; - workOrder.LocationId = model.LocationId; - workOrder.Status = "Open"; - workOrder.istemplate = model.istemplate; - workOrder.BeforPhotoIssue = model.BeforPhotoIssue; - workOrder.AfterPhotoIssue = model.AfterPhotoIssue; - workOrder.SignOffName = model.SignOffName; - workOrder.SignOffSignature = model.SignOffSignature; + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); - workOrder.WorkOrderContacts = workOrder.WorkOrderContacts == null ? new List() : workOrder.WorkOrderContacts; - List contactIds = model.GetContactIds(); - workOrder.WorkOrderCategories = workOrder.WorkOrderCategories == null ? new List() : workOrder.WorkOrderCategories; - List categoryIds = model.GetCategoryIds(); - - // Create new records for the associations (pseudo-code) - foreach (int contactId in contactIds) + var createDto = new CreateWorkOrderDTO { - // Create association with contact - workOrder.WorkOrderContacts.Add(new WorkOrderContacts { ContactId = contactId }); + WorkerOrderTitle = model.WorkerOrderTitle ?? "", + Description = model.Description, + Status = WorkOrderStatus.Open, + Priority = model.Priority ?? WorkOrderPriority.Medium, + LocationId = model.LocationId, + AssignTo = model.AssignTo, + DueDate = model.DueDate + }; + + var workOrderId = await _workOrderService.CreateWorkOrderAsync(createDto, userId!); + + if (model.ContactIds != null && model.ContactIds.Any()) + { + await _workOrderDataService.AddWorkOrderContactsAsync(workOrderId, model.ContactIds); } - foreach (int categoryId in categoryIds) + if (model.CategoryIds != null && model.CategoryIds.Any()) { - // Create association with category - workOrder.WorkOrderCategories.Add(new WorkOrderCategories { CategoryId = categoryId }); + await _workOrderDataService.AddWorkOrderCategoriesAsync(workOrderId, model.CategoryIds); } - #region Images of workorder - workOrder.workOrderAttachments = workOrder.workOrderAttachments == null ? new List() : workOrder.workOrderAttachments; if (model.Attachments != null) { foreach (var attachment in model.Attachments) { - string fileName = ContentDispositionHeaderValue.Parse(attachment.ContentDisposition).FileName.Trim('"'); - string uniqueFileName = $"{Guid.NewGuid()}_{fileName}"; - - string uploadProfilePath = Path.Combine("Assets", "Documents"); - string fullPath = Path.Combine(_webHostEnvironment.WebRootPath, uploadProfilePath, uniqueFileName); - - Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); - - // Copy the file to the server - using (var ms = System.IO.File.Create(fullPath)) + var fileUrl = await SaveFileAsync(attachment); + await _workOrderDataService.AddWorkOrderAttachmentAsync(new WorkOrderAttachments { - await attachment.CopyToAsync(ms); - } - - // Update the user's image URL - var request = _httpContext.HttpContext.Request; - var domain = $"{request.Scheme}://{request.Host}"; - var imagesPath = domain + "/" + Path.Combine(uploadProfilePath, uniqueFileName); - workOrder.workOrderAttachments.Add(new WorkOrderAttachments { Attachments = imagesPath }); + WorkorderId = workOrderId, + Attachments = fileUrl + }); } } if (model.BeforPhotoAttachment != null) { - string fileName = ContentDispositionHeaderValue.Parse(model.BeforPhotoAttachment.ContentDisposition).FileName.Trim('"'); - string uniqueFileName = $"{Guid.NewGuid()}_{fileName}"; - - string uploadProfilePath = Path.Combine("Assets", "Documents"); - string fullPath = Path.Combine(_webHostEnvironment.WebRootPath, uploadProfilePath, uniqueFileName); - - Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); - // Copy the file to the server - using (var ms = System.IO.File.Create(fullPath)) + var fileUrl = await SaveFileAsync(model.BeforPhotoAttachment); + var workOrder = await _workOrderDataService.GetByIdAsync(workOrderId); + if (workOrder != null) { - await model.BeforPhotoAttachment.CopyToAsync(ms); + workOrder.BeforPhotoAttachment = fileUrl; + await _workOrderDataService.UpdateAsync(workOrder); } - // Update the user's image URL - var request = _httpContext.HttpContext.Request; - var domain = $"{request.Scheme}://{request.Host}"; - workOrder.BeforPhotoAttachment = domain + "/" + Path.Combine(uploadProfilePath, uniqueFileName); } + if (model.AfterPhotoAttachment != null) { - string fileName = ContentDispositionHeaderValue.Parse(model.AfterPhotoAttachment.ContentDisposition).FileName.Trim('"'); - string uniqueFileName = $"{Guid.NewGuid()}_{fileName}"; - - string uploadProfilePath = Path.Combine("Assets", "Documents"); - string fullPath = Path.Combine(_webHostEnvironment.WebRootPath, uploadProfilePath, uniqueFileName); - - Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); - // Copy the file to the server - using (var ms = System.IO.File.Create(fullPath)) + var fileUrl = await SaveFileAsync(model.AfterPhotoAttachment); + var workOrder = await _workOrderDataService.GetByIdAsync(workOrderId); + if (workOrder != null) { - await model.AfterPhotoAttachment.CopyToAsync(ms); + workOrder.AfterPhotoAttachment = fileUrl; + await _workOrderDataService.UpdateAsync(workOrder); } - // Update the user's image URL - var request = _httpContext.HttpContext.Request; - var domain = $"{request.Scheme}://{request.Host}"; - workOrder.AfterPhotoAttachment = domain + "/" + Path.Combine(uploadProfilePath, uniqueFileName); } + if (model.SignOffAttachment != null) { - string fileName = ContentDispositionHeaderValue.Parse(model.SignOffAttachment.ContentDisposition).FileName.Trim('"'); - string uniqueFileName = $"{Guid.NewGuid()}_{fileName}"; - - string uploadProfilePath = Path.Combine("Assets", "Documents"); - string fullPath = Path.Combine(_webHostEnvironment.WebRootPath, uploadProfilePath, uniqueFileName); - - Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); - // Copy the file to the server - using (var ms = System.IO.File.Create(fullPath)) + var fileUrl = await SaveFileAsync(model.SignOffAttachment); + var workOrder = await _workOrderDataService.GetByIdAsync(workOrderId); + if (workOrder != null) { - await model.SignOffAttachment.CopyToAsync(ms); + workOrder.SignOffAttachment = fileUrl; + await _workOrderDataService.UpdateAsync(workOrder); } - // Update the user's image URL - var request = _httpContext.HttpContext.Request; - var domain = $"{request.Scheme}://{request.Host}"; - workOrder.SignOffAttachment = domain + "/" + Path.Combine(uploadProfilePath, uniqueFileName); } - #endregion - _db.Add(workOrder); - _db.SaveChanges(); - return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" }); + + return Ok(new DataResponse { Message = "Work order created successfully", Status = "200", Data = workOrderId }); + } + 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) { @@ -196,186 +170,97 @@ namespace Api.SeaHavenIndustries.Controllers [HttpPost] [HttpPut("{id:int}")] [Route("EditWorkorder")] - public async Task Editworkorder([FromBody] EditWorkorder_DTO model) + public async Task Editworkorder([FromForm] EditWorkorder_DTO model) { try { - var workOrder = _db.workOrders.Where(w => w.Id == model.Id).FirstOrDefault(); - if (workOrder == null) - return BadRequest(new Response { Status = "Error", Message = "Work order not found" }); + if (!model.Id.HasValue) + return BadRequest(new Response { Status = "Error", Message = "Work order ID is required" }); var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); - var now = DateTime.UtcNow; - void LogChange(string field, string? oldVal, string? newVal) + + var updateDto = new UpdateWorkOrderDTO { - if (oldVal == newVal) return; - _db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog - { - WorkOrderId = workOrder.Id, - UserId = userId, - FieldName = field, - OldValue = oldVal ?? "", - NewValue = newVal ?? "", - Action = "edit", - CreatedAt = now - }); + WorkerOrderTitle = model.WorkerOrderTitle, + Description = model.Description, + Status = model.Status, + Priority = model.Priority, + LocationId = model.LocationId, + AssignTo = model.AssignTo, + DueDate = model.DueDate, + CompletedDate = model.CompletedDate + }; + + var success = await _workOrderService.UpdateWorkOrderAsync(model.Id.Value, updateDto, userId!); + if (!success) + return BadRequest(new Response { Status = "Error", Message = "Work order not found" }); + + await _workOrderDataService.DeleteWorkOrderContactsAsync(model.Id.Value); + await _workOrderDataService.DeleteWorkOrderCategoriesAsync(model.Id.Value); + + if (model.ContactIds != null && model.ContactIds.Any()) + { + await _workOrderDataService.AddWorkOrderContactsAsync(model.Id.Value, model.ContactIds); } - LogChange("WorkOrderNumber", workOrder.WorkerOrderNumber, model.WorkerOrderNumber); - LogChange("Title", workOrder.WorkerOrderTitle, model.WorkerOrderTitle); - LogChange("Description", workOrder.Description, model.Description); - LogChange("TT", workOrder.TT, model.TT); - LogChange("PO", workOrder.PO, model.PO); - LogChange("AssignTo", workOrder.AssignTo, model.AssignTo); - LogChange("DueDate", workOrder.DueDate?.ToString("yyyy-MM-dd"), model.DueDate?.ToString("yyyy-MM-dd")); - LogChange("Priority", workOrder.Priority, model.Priority); - LogChange("LocationId", workOrder.LocationId?.ToString(), model.LocationId?.ToString()); - LogChange("Status", workOrder.Status, model.Status); - LogChange("Problem", workOrder.Problem, model.Problem); - LogChange("Trade", workOrder.Trade, model.Trade); - LogChange("SubTrade", workOrder.SubTrade, model.SubTrade); - LogChange("VendorNTE", workOrder.VendorNTE?.ToString("F2"), model.VendorNTE?.ToString("F2")); - LogChange("ScheduledDate", workOrder.ScheduledDate?.ToString("yyyy-MM-dd"), model.ScheduledDate?.ToString("yyyy-MM-dd")); - LogChange("CompletedDate", workOrder.CompletedDate?.ToString("yyyy-MM-dd"), model.CompletedDate?.ToString("yyyy-MM-dd")); - LogChange("Source", workOrder.Source, model.Source); - - workOrder.WorkerOrderNumber = model.WorkerOrderNumber; - workOrder.WorkerOrderTitle = model.WorkerOrderTitle; - workOrder.Description = model.Description; - workOrder.TT = model.TT; - workOrder.PO = model.PO; - workOrder.AssignTo = model.AssignTo; - workOrder.DueDate = model.DueDate; - workOrder.Priority = model.Priority; - workOrder.LocationId = model.LocationId; - workOrder.Status = model.Status; - workOrder.Problem = model.Problem; - workOrder.Trade = model.Trade; - workOrder.SubTrade = model.SubTrade; - workOrder.VendorNTE = model.VendorNTE; - workOrder.ScheduledDate = model.ScheduledDate; - workOrder.CompletedDate = model.CompletedDate; - workOrder.Source = model.Source; - workOrder.BeforPhotoIssue = model.BeforPhotoIssue; - workOrder.AfterPhotoIssue = model.AfterPhotoIssue; - workOrder.SignOffName = model.SignOffName; - workOrder.SignOffSignature = model.SignOffSignature; - var existcontact = _db.WorkOrderContacts.Where(w => w.WorkorderId == model.Id).ToList(); - if (existcontact.Count() > 0) + if (model.CategoryIds != null && model.CategoryIds.Any()) { - _db.WorkOrderContacts.RemoveRange(existcontact); - } - var existcategory = _db.workOrderCategories.Where(w => w.WorkorderId == model.Id).ToList(); - if (existcategory.Count() > 0) - { - _db.workOrderCategories.RemoveRange(existcategory); - } - workOrder.WorkOrderContacts = workOrder.WorkOrderContacts == null ? new List() : workOrder.WorkOrderContacts; - List contactIds = model.GetContactIds(); - - workOrder.WorkOrderCategories = workOrder.WorkOrderCategories == null ? new List() : workOrder.WorkOrderCategories; - List categoryIds = model.GetCategoryIds(); - - // Create new records for the associations (pseudo-code) - foreach (int contactId in contactIds) - { - // Create association with contact - workOrder.WorkOrderContacts.Add(new WorkOrderContacts { ContactId = contactId }); + await _workOrderDataService.AddWorkOrderCategoriesAsync(model.Id.Value, model.CategoryIds); } - foreach (int categoryId in categoryIds) - { - // Create association with category - workOrder.WorkOrderCategories.Add(new WorkOrderCategories { CategoryId = categoryId }); - } - - #region Images of workorder - workOrder.workOrderAttachments = workOrder.workOrderAttachments == null ? new List() : workOrder.workOrderAttachments; if (model.Attachments != null) { foreach (var attachment in model.Attachments) { - string fileName = ContentDispositionHeaderValue.Parse(attachment.ContentDisposition).FileName.Trim('"'); - string uniqueFileName = $"{Guid.NewGuid()}_{fileName}"; - - string uploadProfilePath = Path.Combine("Assets", "Documents"); - string fullPath = Path.Combine(_webHostEnvironment.WebRootPath, uploadProfilePath, uniqueFileName); - - Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); - - using (var ms = System.IO.File.Create(fullPath)) + var fileUrl = await SaveFileAsync(attachment); + await _workOrderDataService.AddWorkOrderAttachmentAsync(new WorkOrderAttachments { - await attachment.CopyToAsync(ms); - } - - var request = _httpContext.HttpContext.Request; - var domain = $"{request.Scheme}://{request.Host}"; - var imagesPath = domain + "/" + Path.Combine(uploadProfilePath, uniqueFileName); - workOrder.workOrderAttachments.Add(new WorkOrderAttachments { Attachments = imagesPath }); + WorkorderId = model.Id.Value, + Attachments = fileUrl + }); } } if (model.BeforPhotoAttachment != null) { - string fileName = ContentDispositionHeaderValue.Parse(model.BeforPhotoAttachment.ContentDisposition).FileName.Trim('"'); - string uniqueFileName = $"{Guid.NewGuid()}_{fileName}"; - - string uploadProfilePath = Path.Combine("Assets", "Documents"); - string fullPath = Path.Combine(_webHostEnvironment.WebRootPath, uploadProfilePath, uniqueFileName); - - Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); - // Copy the file to the server - using (var ms = System.IO.File.Create(fullPath)) + var fileUrl = await SaveFileAsync(model.BeforPhotoAttachment); + var workOrder = await _workOrderDataService.GetByIdAsync(model.Id.Value); + if (workOrder != null) { - await model.BeforPhotoAttachment.CopyToAsync(ms); + workOrder.BeforPhotoAttachment = fileUrl; + await _workOrderDataService.UpdateAsync(workOrder); } - // Update the user's image URL - var request = _httpContext.HttpContext.Request; - var domain = $"{request.Scheme}://{request.Host}"; - workOrder.BeforPhotoAttachment = domain + "/" + Path.Combine(uploadProfilePath, uniqueFileName); } + if (model.AfterPhotoAttachment != null) { - string fileName = ContentDispositionHeaderValue.Parse(model.AfterPhotoAttachment.ContentDisposition).FileName.Trim('"'); - string uniqueFileName = $"{Guid.NewGuid()}_{fileName}"; - - string uploadProfilePath = Path.Combine("Assets", "Documents"); - string fullPath = Path.Combine(_webHostEnvironment.WebRootPath, uploadProfilePath, uniqueFileName); - - Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); - // Copy the file to the server - using (var ms = System.IO.File.Create(fullPath)) + var fileUrl = await SaveFileAsync(model.AfterPhotoAttachment); + var workOrder = await _workOrderDataService.GetByIdAsync(model.Id.Value); + if (workOrder != null) { - await model.AfterPhotoAttachment.CopyToAsync(ms); + workOrder.AfterPhotoAttachment = fileUrl; + await _workOrderDataService.UpdateAsync(workOrder); } - // Update the user's image URL - var request = _httpContext.HttpContext.Request; - var domain = $"{request.Scheme}://{request.Host}"; - workOrder.AfterPhotoAttachment = domain + "/" + Path.Combine(uploadProfilePath, uniqueFileName); } + if (model.SignOffAttachment != null) { - string fileName = ContentDispositionHeaderValue.Parse(model.SignOffAttachment.ContentDisposition).FileName.Trim('"'); - string uniqueFileName = $"{Guid.NewGuid()}_{fileName}"; - - string uploadProfilePath = Path.Combine("Assets", "Documents"); - string fullPath = Path.Combine(_webHostEnvironment.WebRootPath, uploadProfilePath, uniqueFileName); - - Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); - // Copy the file to the server - using (var ms = System.IO.File.Create(fullPath)) + var fileUrl = await SaveFileAsync(model.SignOffAttachment); + var workOrder = await _workOrderDataService.GetByIdAsync(model.Id.Value); + if (workOrder != null) { - await model.SignOffAttachment.CopyToAsync(ms); + workOrder.SignOffAttachment = fileUrl; + await _workOrderDataService.UpdateAsync(workOrder); } - // Update the user's image URL - var request = _httpContext.HttpContext.Request; - var domain = $"{request.Scheme}://{request.Host}"; - workOrder.SignOffAttachment = domain + "/" + Path.Combine(uploadProfilePath, uniqueFileName); } - #endregion - _db.SaveChanges(); + return Ok(new DataResponse { Message = "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 (Exception ex) { return BadRequest(new Response { Status = "Error", Message = ex.Message }); @@ -412,7 +297,7 @@ namespace Api.SeaHavenIndustries.Controllers [HttpDelete] [Route("DeleteWorkorder")] - public IActionResult Deleteworkorder([FromBody] EditWorkorder_DTO model) + public async Task Deleteworkorder([FromBody] EditWorkorder_DTO model) { int id = model.Id.HasValue ? model.Id.Value : 0; if (id == 0) @@ -421,46 +306,20 @@ namespace Api.SeaHavenIndustries.Controllers } try { - var exist = _db.workOrders.Where(w => w.Id == id).FirstOrDefault(); - if (exist != null) - { - var quotes = _db.Quotes.Where(w => w.WorkorderId == id).ToList(); - if (quotes.Count() > 0) - { - foreach (var itm in quotes) - { - var existquotes = _db.Quotes.Where(w => w.Id == itm.Id).FirstOrDefault(); - existquotes.WorkorderId = null; - } - } - var attachments = _db.workOrderAttachments.Where(w => w.WorkorderId == id).ToList(); - if (attachments.Count() > 0) - { - _db.RemoveRange(attachments); - } - var contacts = _db.WorkOrderContacts.Where(w => w.WorkorderId == id).ToList(); - if (contacts.Count() > 0) - { - _db.RemoveRange(contacts); - } - var category = _db.workOrderCategories.Where(w => w.WorkorderId == id).ToList(); - if (category.Count() > 0) - { - _db.RemoveRange(category); - } - var comments = _db.Comments.Where(w => w.WorkerOrderId == id).ToList(); - if (comments.Count() > 0) - { - _db.RemoveRange(comments); - } - _db.Remove(exist); + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + var success = await _workOrderService.DeleteWorkOrderAsync(id, userId!); - } - else + if (!success) + return BadRequest("Work order not found!"); + + await _quotesDataService.UnlinkFromWorkOrderAsync(id); + + var comments = await _commentDataService.GetByWorkOrderIdAsync(id); + foreach (var comment in comments) { - return BadRequest("ID not matched!"); + await _commentDataService.DeleteAsync(comment.Id); } - _db.SaveChanges(); + return Ok(new DataResponse { Message = "Removed Successfully", Status = "200" }); } catch (Exception ex) @@ -782,25 +641,30 @@ namespace Api.SeaHavenIndustries.Controllers { try { - var data = await _db.workOrders.Where(w => w.Id == id).FirstOrDefaultAsync(); - if (data == null) + var workOrder = await _workOrderDataService.GetByIdAsync(id); + if (workOrder == null) return BadRequest(new Response { Status = "Error", Message = "Work order not found" }); - if (data.Status != status) + + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + + if (workOrder.Status != status) { - _db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog + await _workOrderDataService.AddAuditLogAsync(new WorkOrderAuditLog { WorkOrderId = id, - UserId = User.FindFirstValue(ClaimTypes.NameIdentifier), + UserId = userId, FieldName = "Status", - OldValue = data.Status ?? "", + OldValue = workOrder.Status ?? "", NewValue = status, - Action = "status_change", + Action = AuditAction.StatusChange.ToString(), CreatedAt = DateTime.UtcNow }); } - data.Status = status; - await _db.SaveChangesAsync(); - return Ok(new DataResponse { Message = "Updated Successfully", Status = "200", Data = data }); + + workOrder.Status = status; + await _workOrderDataService.UpdateAsync(workOrder); + + return Ok(new DataResponse { Message = "Updated Successfully", Status = "200", Data = WorkOrderDTO.MapToModel(workOrder) }); } catch (Exception ex) { @@ -814,28 +678,33 @@ namespace Api.SeaHavenIndustries.Controllers { try { - var data = await _db.workOrders.Where(w => w.Id == id).FirstOrDefaultAsync(); - if (data == null) + var workOrder = await _workOrderDataService.GetByIdAsync(id); + if (workOrder == null) return BadRequest(new Response { Status = "Error", Message = "Work order not found" }); - if (data.AssignTo != assignTo) + + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + + if (workOrder.AssignTo != assignTo) { - var oldUser = data.AssignTo != null ? await _db.Users.FindAsync(data.AssignTo) : null; - var newUser = assignTo != null ? await _db.Users.FindAsync(assignTo) : null; - _db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog + var oldUser = workOrder.AssignTo != null ? await _userDataService.GetByIdAsync(workOrder.AssignTo) : null; + var newUser = assignTo != null ? await _userDataService.GetByIdAsync(assignTo) : null; + + await _workOrderDataService.AddAuditLogAsync(new WorkOrderAuditLog { WorkOrderId = id, - UserId = User.FindFirstValue(ClaimTypes.NameIdentifier), + UserId = userId, FieldName = "AssignedTo", OldValue = oldUser != null ? (oldUser.FirstName + " " + oldUser.LastName).Trim() : "", NewValue = newUser != null ? (newUser.FirstName + " " + newUser.LastName).Trim() : "Unassigned", - Action = "assignment_change", + Action = AuditAction.AssignmentChange.ToString(), CreatedAt = DateTime.UtcNow }); } - data.AssignTo = assignTo; - await _db.SaveChangesAsync(); - var assignedUser = assignTo != null ? await _db.Users.FindAsync(assignTo) : null; + workOrder.AssignTo = assignTo; + await _workOrderDataService.UpdateAsync(workOrder); + + var assignedUser = assignTo != null ? await _userDataService.GetByIdAsync(assignTo) : null; var assignedName = assignedUser != null ? (assignedUser.FirstName + " " + assignedUser.LastName).Trim() : ""; return Ok(new { message = "Updated Successfully", assignedTo = assignedName }); } @@ -852,34 +721,23 @@ namespace Api.SeaHavenIndustries.Controllers try { var userId = _httpContext?.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? ""; - Comments comments = new Comments(); - comments.CreatedDate = DateTime.Now; - comments.UserId = userId; - comments.WorkerOrderId = model.WorkorderId; - comments.Commenttext = model.Text; - comments.CommentType = model.CommentType ?? "vendor"; + + var comment = new Comments + { + UserId = userId, + WorkerOrderId = model.WorkorderId, + Commenttext = model.Text, + CommentType = model.CommentType ?? "vendor" + }; + if (model.Document != null) { - string fileName = ContentDispositionHeaderValue.Parse(model.Document.ContentDisposition).FileName.Trim('"'); - string uniqueFileName = $"{Guid.NewGuid()}_{fileName}"; - - string uploadProfilePath = Path.Combine("Assets", "Documents"); - string fullPath = Path.Combine(_webHostEnvironment.WebRootPath, uploadProfilePath, uniqueFileName); - - Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); - // Copy the file to the server - using (var ms = System.IO.File.Create(fullPath)) - { - await model.Document.CopyToAsync(ms); - } - // Update the user's image URL - var request = _httpContext.HttpContext.Request; - var domain = $"{request.Scheme}://{request.Host}"; - comments.Documents = domain + "/" + Path.Combine(uploadProfilePath, uniqueFileName); + var fileUrl = await SaveFileAsync(model.Document); + comment.Documents = fileUrl; } - _db.Comments.Add(comments); - await _db.SaveChangesAsync(); - return Ok(new DataResponse { Message = "Updated Successfully", Status = "200", Data = comments }); + + var saved = await _commentDataService.AddAsync(comment); + return Ok(new DataResponse { Message = "Updated Successfully", Status = "200", Data = saved }); } catch (Exception ex) { @@ -896,22 +754,21 @@ namespace Api.SeaHavenIndustries.Controllers var userId = _httpContext?.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? ""; var comment = new Comments { - CreatedDate = DateTime.UtcNow, UserId = userId, WorkerOrderId = model.WorkorderId, Commenttext = model.Text, CommentType = model.CommentType ?? "internal", }; - _db.Comments.Add(comment); - await _db.SaveChangesAsync(); - var user = await _db.Users.FindAsync(userId); + var saved = await _commentDataService.AddAsync(comment); + var user = await _userDataService.GetByIdAsync(userId); + return Ok(new { - comment.Id, - comment.CreatedDate, - comment.Commenttext, - comment.CommentType, + saved.Id, + saved.CreatedDate, + saved.Commenttext, + saved.CommentType, userName = user != null ? (user.FirstName + " " + user.LastName).Trim() : "" }); } @@ -925,13 +782,14 @@ namespace Api.SeaHavenIndustries.Controllers [Route("GetComments")] public async Task GetComments() { - var data = await _db.Comments.Include(i => i.ApplicationUser).Select(s => new + var comments = await _commentDataService.GetAllAsync(); + var data = comments.Select(s => new { s.Id, s.Commenttext, FirstName = s.ApplicationUser != null ? s.ApplicationUser.FirstName : "", s.Documents - }).ToListAsync(); + }).ToList(); return Ok(data); } @@ -939,47 +797,42 @@ namespace Api.SeaHavenIndustries.Controllers [Route("GetCommentsByWorkorderId")] public async Task GetCommentsByWorkorderId(int woid) { - var data = await _db.Comments.Where(w => w.WorkerOrderId == woid).Include(i => i.ApplicationUser).Select(s => new + var comments = await _commentDataService.GetByWorkOrderIdAsync(woid); + var data = comments.Select(s => new { s.Id, s.Commenttext, FirstName = s.ApplicationUser != null ? s.ApplicationUser.FirstName : "", s.Documents - }).ToListAsync(); + }).ToList(); return Ok(data); } [HttpGet] [Route("GetworkordersDD")] public async Task GetworkordersDD() { - _db.ChangeTracker.Clear(); - - var data = await _db.workOrders.Where(w => w.istemplate != true).Include(w => w.Locations).Include(w => w.AssignToUser).ToListAsync(); - return Ok(data); + var data = await _workOrderDataService.GetAllAsync(); + return Ok(data.Where(w => w.istemplate != true)); } [HttpGet] [Route("Getworkorders")] public async Task Getworkorders() { - _db.ChangeTracker.Clear(); - var data = await _db.workOrders - .Where(w => w.istemplate != true) - .AsSplitQuery() - .Include(w => w.Locations) - .Include(w => w.AssignToUser).Select(s => new WorkorderFilterVM - { - Id = s.Id, - Priority = s.Priority, - Status = s.Status, - WorkerOrderNumber = s.WorkerOrderNumber, - WorkerOrderTitle = s.WorkerOrderTitle, - DueDate = s.DueDate, - AssignToUserName = s.AssignToUser != null ? s.AssignToUser.FirstName : "", - LocationName = s.Locations != null ? s.Locations.Name : "", - LocationId = s.LocationId, - PO = s.PO, - TT = s.TT, - }).ToListAsync(); + var workOrders = await _workOrderDataService.GetAllWithDetailsAsync(); + var data = workOrders.Select(s => new WorkorderFilterVM + { + Id = s.Id, + Priority = s.Priority, + Status = s.Status, + WorkerOrderNumber = s.WorkerOrderNumber, + WorkerOrderTitle = s.WorkerOrderTitle, + DueDate = s.DueDate, + AssignToUserName = s.AssignToUser != null ? s.AssignToUser.FirstName : "", + LocationName = s.Locations != null ? s.Locations.Name : "", + LocationId = s.LocationId, + PO = s.PO, + TT = s.TT, + }).ToList(); return Ok(data); } [HttpPost] @@ -988,40 +841,28 @@ namespace Api.SeaHavenIndustries.Controllers { try { - var workOrders = await _db.workOrders.Include(w => w.Locations) - .Where(w => model.WorkOrderIds.Contains(w.Id)) - .ToListAsync(); + var allWorkOrders = await _workOrderDataService.GetAllWithDetailsAsync(); + var workOrders = allWorkOrders.Where(w => model.WorkOrderIds.Contains(w.Id)).ToList(); + if (workOrders.Count == 0) return BadRequest(new Response { Status = "Error", Message = "No work orders found" }); var primaryWO = workOrders.First(); - var lastPO = await _db.Dispatches - .Where(d => d.PONumber != null) - .OrderByDescending(d => d.PONumber) - .Select(d => d.PONumber) - .FirstOrDefaultAsync(); - int nextPO = 1; - if (lastPO != null && lastPO.StartsWith("VPO-") && int.TryParse(lastPO.Substring(4), out var lastNum)) - nextPO = lastNum + 1; + var lastPO = await _dispatchDataService.GenerateNextPONumberAsync(); + int nextPO = int.TryParse(lastPO, out var po) ? po : 1; - var lastDSP = await _db.Dispatches - .Where(d => d.DispatchNumber != null) - .OrderByDescending(d => d.DispatchNumber) - .Select(d => d.DispatchNumber) - .FirstOrDefaultAsync(); - int nextDSP = 1; - if (lastDSP != null && lastDSP.StartsWith("DSP-") && int.TryParse(lastDSP.Substring(4), out var lastDspNum)) - nextDSP = lastDspNum + 1; + var lastDSP = await _dispatchDataService.GenerateNextDispatchNumberAsync(); + int nextDSP = lastDSP.StartsWith("DSP-") && int.TryParse(lastDSP.Substring(4), out var dspNum) ? dspNum : 1; var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); - var currentUser = userId != null ? await _db.Users.FindAsync(userId) : null; + var currentUser = userId != null ? await _userDataService.GetByIdAsync(userId) : null; var senderName = currentUser != null ? (currentUser.FirstName + " " + currentUser.LastName).Trim() : ""; var dispatches = new List(); foreach (var vendorId in model.VendorIds) { - var vendor = await _db.Vendors.FindAsync(vendorId); + var vendor = await _vendorDataService.GetByIdAsync(vendorId); if (vendor == null) continue; var poNumber = $"VPO-{nextPO:D5}"; @@ -1049,9 +890,7 @@ namespace Api.SeaHavenIndustries.Controllers var checklistItems = new List(); if (model.TaskListTemplateId.HasValue) { - var template = await _db.TaskListTemplates - .Include(t => t.Items) - .FirstOrDefaultAsync(t => t.Id == model.TaskListTemplateId.Value); + var template = await _taskListTemplateDataService.GetByIdWithItemsAsync(model.TaskListTemplateId.Value); if (template?.Items != null) { checklistItems.AddRange(template.Items.OrderBy(i => i.SortOrder).Select((i, idx) => @@ -1108,26 +947,25 @@ namespace Api.SeaHavenIndustries.Controllers var emailSent = await _sendMessage.SendDispatchEmail(vendor.Email, subject, html, replyTo); dispatch.EmailSent = emailSent; - _db.Dispatches.Add(dispatch); + var savedDispatch = await _dispatchDataService.AddAsync(dispatch); foreach (var wo in workOrders) { - _db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog + await _workOrderDataService.AddAuditLogAsync(new WorkOrderAuditLog { WorkOrderId = wo.Id, UserId = userId, FieldName = "Dispatch", OldValue = "", NewValue = $"{vendor.CompanyName} ({dispatchNumber})", - Action = "dispatch", + Action = AuditAction.Dispatch.ToString(), CreatedAt = DateTime.UtcNow }); } - dispatches.Add(new { dispatch.Id, dispatchNumber, poNumber, vendorName = vendor.CompanyName, dispatch.Status, workOrderCount = workOrders.Count }); + dispatches.Add(new { savedDispatch.Id, dispatchNumber, poNumber, vendorName = vendor.CompanyName, savedDispatch.Status, workOrderCount = workOrders.Count }); } - await _db.SaveChangesAsync(); return Ok(new { message = "Dispatched", dispatches }); } catch (Exception ex) @@ -1139,40 +977,23 @@ namespace Api.SeaHavenIndustries.Controllers [HttpGet("GetDispatches/{workOrderId}")] public async Task GetDispatches(int workOrderId) { - var dispatchIds = await _db.DispatchWorkOrders - .Where(dwo => dwo.WorkOrderId == workOrderId) - .Select(dwo => dwo.DispatchId) - .Distinct() - .ToListAsync(); - - var directIds = await _db.Dispatches - .Where(d => d.WorkOrderId == workOrderId) - .Select(d => d.Id) - .ToListAsync(); - - var allIds = dispatchIds.Union(directIds).Distinct().ToList(); - - var data = await _db.Dispatches - .Where(d => allIds.Contains(d.Id)) - .Include(d => d.Vendor) - .OrderByDescending(d => d.DispatchedAt) - .Select(d => new - { - d.Id, - d.DispatchNumber, - d.PONumber, - d.NTEAmount, - d.Description, - d.Status, - d.ScheduledDate, - d.CompletedDate, - d.DispatchedAt, - d.EmailSent, - vendorName = d.Vendor != null ? d.Vendor.CompanyName : "", - vendorEmail = d.Vendor != null ? d.Vendor.Email : "", - vendorId = d.VendorId - }) - .ToListAsync(); + var dispatches = await _dispatchDataService.GetByWorkOrderIdAsync(workOrderId); + var data = dispatches.OrderByDescending(d => d.DispatchedAt).Select(d => new + { + d.Id, + d.DispatchNumber, + d.PONumber, + d.NTEAmount, + d.Description, + d.Status, + d.ScheduledDate, + d.CompletedDate, + d.DispatchedAt, + d.EmailSent, + vendorName = d.Vendor != null ? d.Vendor.CompanyName : "", + vendorEmail = d.Vendor != null ? d.Vendor.Email : "", + vendorId = d.VendorId + }).ToList(); return Ok(data); } @@ -1448,24 +1269,22 @@ namespace Api.SeaHavenIndustries.Controllers [HttpPost("UpdateChecklistItem")] public async Task UpdateChecklistItem([FromBody] ChecklistItemUpdate_DTO model) { - var item = await _db.DispatchChecklistItems.FindAsync(model.Id); + var item = await _dispatchDataService.GetChecklistItemByIdAsync(model.Id); if (item == null) return NotFound(new Response { Status = "Error", Message = "Checklist item not found" }); item.IsCompleted = model.IsCompleted; item.CompletedBy = model.IsCompleted ? model.CompletedBy : null; item.CompletedAt = model.IsCompleted ? DateTime.UtcNow : null; - - await _db.SaveChangesAsync(); + await _dispatchDataService.UpdateChecklistItemAsync(item); return Ok(new { message = "Updated", item.Id, item.IsCompleted, item.CompletedBy, item.CompletedAt }); } [HttpPost("AddChecklistItem")] public async Task AddChecklistItem([FromBody] AddChecklistItem_DTO model) { - var maxSort = await _db.DispatchChecklistItems - .Where(c => c.DispatchId == model.DispatchId) - .MaxAsync(c => (int?)c.SortOrder) ?? -1; + var checklistItems = await _dispatchDataService.GetChecklistItemsAsync(model.DispatchId); + var maxSort = checklistItems.Any() ? checklistItems.Max(c => c.SortOrder) : -1; var item = new DispatchChecklistItem { @@ -1475,24 +1294,32 @@ namespace Api.SeaHavenIndustries.Controllers SortOrder = maxSort + 1, IsCompleted = false }; - _db.DispatchChecklistItems.Add(item); - await _db.SaveChangesAsync(); + await _dispatchDataService.AddChecklistItemAsync(item); return Ok(new { item.Id, item.ItemText, item.SortOrder, item.IsCompleted }); } private async Task GenerateInternalWONumber() { - var lastWO = await _db.workOrders - .Where(w => w.InternalWONumber != null) - .OrderByDescending(w => w.InternalWONumber) - .Select(w => w.InternalWONumber) - .FirstOrDefaultAsync(); + return await _workOrderDataService.GenerateNextInternalWONumberAsync(); + } - int next = 10000001; - if (lastWO != null && int.TryParse(lastWO, out var lastNum)) - next = lastNum + 1; + private async Task SaveFileAsync(IFormFile file) + { + var fileName = ContentDispositionHeaderValue.Parse(file.ContentDisposition).FileName.Trim('"'); + var uniqueFileName = $"{Guid.NewGuid()}_{fileName}"; + var uploadPath = Path.Combine("Assets", "Documents"); + var fullPath = Path.Combine(_webHostEnvironment.WebRootPath, uploadPath, uniqueFileName); - return next.ToString("D8"); + Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!); + + using (var stream = System.IO.File.Create(fullPath)) + { + await file.CopyToAsync(stream); + } + + var request = _httpContext.HttpContext!.Request; + var domain = $"{request.Scheme}://{request.Host}"; + return $"{domain}/{uploadPath.Replace("\\", "/")}/{uniqueFileName}"; } public class WorkorderFilterVM diff --git a/Api.SeaHavenIndustries/DTOs/Workorder_DTO.cs b/Api.SeaHavenIndustries/DTOs/Workorder_DTO.cs index 8b8f4e5..32d6532 100644 --- a/Api.SeaHavenIndustries/DTOs/Workorder_DTO.cs +++ b/Api.SeaHavenIndustries/DTOs/Workorder_DTO.cs @@ -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? 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 GetContactIds() + public List? ContactIds { get; set; } + public List? CategoryIds { get; set; } + + public static WorkOrder MapToEntity(Workorder_DTO dto, string userId) { - return WorkOrderContacts?.Split(',').Select(int.Parse).ToList() ?? new List(); - } - public List GetCategoryIds() - { - return WorkOrderCategories?.Split(',').Select(int.Parse).ToList() ?? new List(); + 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? 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 GetContactIds() + public List? ContactIds { get; set; } + public List? CategoryIds { get; set; } + + public static void UpdateEntity(EditWorkorder_DTO dto, WorkOrder entity) { - return WorkOrderContacts?.Split(',').Select(int.Parse).ToList() ?? new List(); - } - public List GetCategoryIds() - { - return WorkOrderCategories?.Split(',').Select(int.Parse).ToList() ?? new List(); + 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; } } - } \ No newline at end of file diff --git a/Api.SeaHavenIndustries/Program.cs b/Api.SeaHavenIndustries/Program.cs index 42c0309..a826847 100644 --- a/Api.SeaHavenIndustries/Program.cs +++ b/Api.SeaHavenIndustries/Program.cs @@ -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(); builder.Services.AddScoped(); builder.Services.AddSingleton(sp => @@ -43,36 +45,8 @@ builder.Services.AddSingleton(sp return zipService; }); -// ======================================================================== -// MULTI-TIER ARCHITECTURE - Dependency Injection Registration -// ======================================================================== - -// Data Services (Data Access Layer) -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -// Business Services (Business Logic Layer) -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -// ======================================================================== +builder.Services.AddDataServices(); +builder.Services.AddBusinessServices(); builder.Services.AddAuthentication(options => { diff --git a/BACKEND_ARCHITECTURE.md b/BACKEND_ARCHITECTURE.md new file mode 100644 index 0000000..9787f11 --- /dev/null +++ b/BACKEND_ARCHITECTURE.md @@ -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 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 _createValidator; + + public ContactService( + IContactDataService dataService, + IValidator createValidator) + { + _dataService = dataService; + _createValidator = createValidator; + } + + public async Task 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 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 +{ + 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 CreateContactAsync(CreateContactDTO dto, string userId); + Task UpdateContactAsync(int id, UpdateContactDTO dto, string userId); + Task DeleteContactAsync(int id, string userId); + Task GetContactByIdAsync(int id); + Task> GetContactsPagedAsync(int page, int pageSize, string? search); +} +``` + +### Data Service Interface (`IContactDataService.cs`) +```csharp +public interface IContactDataService +{ + Task CreateAsync(CreateContactDataDTO dto, string userId); + Task UpdateAsync(int id, UpdateContactDataDTO dto, string userId); + Task DeleteAsync(int id, string userId); + Task GetByIdAsync(int id); + Task> 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 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! diff --git a/DI_ASSEMBLY_SCANNING_GUIDE.md b/DI_ASSEMBLY_SCANNING_GUIDE.md new file mode 100644 index 0000000..b678c9c --- /dev/null +++ b/DI_ASSEMBLY_SCANNING_GUIDE.md @@ -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(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +// ... 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(); // ❌ 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`? +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` diff --git a/Data.SeaHavenIndustries/Enums/WorkOrderEnums.cs b/Data.SeaHavenIndustries/Enums/WorkOrderEnums.cs new file mode 100644 index 0000000..9c2e454 --- /dev/null +++ b/Data.SeaHavenIndustries/Enums/WorkOrderEnums.cs @@ -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 + } +} diff --git a/QUICK_START_GUIDE.md b/QUICK_START_GUIDE.md new file mode 100644 index 0000000..c116639 --- /dev/null +++ b/QUICK_START_GUIDE.md @@ -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 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 +{ + 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 CreateAsync(CreateProductDataDTO dto, string userId); + Task UpdateAsync(int id, UpdateProductDataDTO dto, string userId); + Task DeleteAsync(int id, string userId); + Task GetByIdAsync(int id); + Task> 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 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 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 CreateProductAsync(CreateProductDTO dto, string userId); + Task UpdateProductAsync(int id, UpdateProductDTO dto, string userId); + Task DeleteProductAsync(int id, string userId); + Task GetProductByIdAsync(int id); + Task> 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 _createValidator; + + public ProductService( + IProductDataService dataService, + IValidator createValidator) + { + _dataService = dataService; + _createValidator = createValidator; + } + + public async Task 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 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 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 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 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 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 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` diff --git a/SeaHaven.DataServices/DependencyInjection/DataServicesModule.cs b/SeaHaven.DataServices/DependencyInjection/DataServicesModule.cs new file mode 100644 index 0000000..abec585 --- /dev/null +++ b/SeaHaven.DataServices/DependencyInjection/DataServicesModule.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.DependencyInjection; +using System.Reflection; + +namespace SeaHaven.DataServices.DependencyInjection +{ + public static class DataServicesModule + { + private static readonly List 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; + } + } +} diff --git a/SeaHaven.DataServices/Implementation/CommentDataService.cs b/SeaHaven.DataServices/Implementation/CommentDataService.cs new file mode 100644 index 0000000..72db1a1 --- /dev/null +++ b/SeaHaven.DataServices/Implementation/CommentDataService.cs @@ -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 GetByIdAsync(int id) + { + return await _context.Comments.FindAsync(id); + } + + public async Task> GetAllAsync() + { + return await _context.Comments + .Include(c => c.ApplicationUser) + .ToListAsync(); + } + + public async Task> GetByWorkOrderIdAsync(int workOrderId) + { + return await _context.Comments + .Where(c => c.WorkerOrderId == workOrderId) + .Include(c => c.ApplicationUser) + .OrderBy(c => c.CreatedDate) + .ToListAsync(); + } + + public async Task> GetByDispatchIdAsync(int dispatchId) + { + return await _context.Comments + .Where(c => c.DispatchId == dispatchId) + .Include(c => c.ApplicationUser) + .OrderBy(c => c.CreatedDate) + .ToListAsync(); + } + + public async Task 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 ExistsAsync(int id) + { + return await _context.Comments.AnyAsync(c => c.Id == id); + } + } +} diff --git a/SeaHaven.DataServices/Implementation/DispatchDataService.cs b/SeaHaven.DataServices/Implementation/DispatchDataService.cs index b4472a8..4a057f6 100644 --- a/SeaHaven.DataServices/Implementation/DispatchDataService.cs +++ b/SeaHaven.DataServices/Implementation/DispatchDataService.cs @@ -111,5 +111,110 @@ namespace SeaHaven.DataServices.Implementation { return await _context.Dispatches.CountAsync(); } + + public async Task> 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> GetByWorkOrderIdsAsync(IEnumerable workOrderIds) + { + return await _context.Dispatches + .Where(d => workOrderIds.Contains(d.WorkOrderId ?? 0)) + .Include(d => d.Vendor) + .ToListAsync(); + } + + public async Task 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> GetChecklistItemsAsync(int dispatchId) + { + return await _context.DispatchChecklistItems + .Where(ci => ci.DispatchId == dispatchId) + .OrderBy(ci => ci.SortOrder) + .ToListAsync(); + } + + public async Task 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> 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 HasSignoffTypeAsync(int dispatchId, string signoffType) + { + return await _context.DispatchSignoffs + .AnyAsync(s => s.DispatchId == dispatchId && s.SignoffType == signoffType); + } + + public async Task> 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(); + } } } diff --git a/SeaHaven.DataServices/Implementation/QuotesDataService.cs b/SeaHaven.DataServices/Implementation/QuotesDataService.cs new file mode 100644 index 0000000..44fc3f5 --- /dev/null +++ b/SeaHaven.DataServices/Implementation/QuotesDataService.cs @@ -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 GetByIdAsync(int id) + { + return await _context.Quotes.FindAsync(id); + } + + public async Task> GetAllAsync() + { + return await _context.Quotes.ToListAsync(); + } + + public async Task> GetByWorkOrderIdAsync(int workOrderId) + { + return await _context.Quotes + .Where(q => q.WorkorderId == workOrderId) + .ToListAsync(); + } + + public async Task 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 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(); + } + } +} diff --git a/SeaHaven.DataServices/Implementation/TaskListTemplateDataService.cs b/SeaHaven.DataServices/Implementation/TaskListTemplateDataService.cs new file mode 100644 index 0000000..f0ce1af --- /dev/null +++ b/SeaHaven.DataServices/Implementation/TaskListTemplateDataService.cs @@ -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 GetByIdAsync(int id) + { + return await _context.TaskListTemplates.FindAsync(id); + } + + public async Task GetByIdWithItemsAsync(int id) + { + return await _context.TaskListTemplates + .Include(t => t.Items) + .FirstOrDefaultAsync(t => t.Id == id); + } + + public async Task> GetAllAsync() + { + return await _context.TaskListTemplates.ToListAsync(); + } + + public async Task 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 ExistsAsync(int id) + { + return await _context.TaskListTemplates.AnyAsync(t => t.Id == id); + } + } +} diff --git a/SeaHaven.DataServices/Implementation/UserDataService.cs b/SeaHaven.DataServices/Implementation/UserDataService.cs new file mode 100644 index 0000000..365ceca --- /dev/null +++ b/SeaHaven.DataServices/Implementation/UserDataService.cs @@ -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 GetByIdAsync(string id) + { + return await _context.Users.FindAsync(id); + } + + public async Task> GetAllAsync() + { + return await _context.Users.ToListAsync(); + } + + public async Task GetByEmailAsync(string email) + { + return await _context.Users + .FirstOrDefaultAsync(u => u.Email == email); + } + + public async Task ExistsAsync(string id) + { + return await _context.Users.AnyAsync(u => u.Id == id); + } + } +} diff --git a/SeaHaven.DataServices/Implementation/WorkOrderDataService.cs b/SeaHaven.DataServices/Implementation/WorkOrderDataService.cs index d1107bd..9509840 100644 --- a/SeaHaven.DataServices/Implementation/WorkOrderDataService.cs +++ b/SeaHaven.DataServices/Implementation/WorkOrderDataService.cs @@ -239,6 +239,89 @@ namespace SeaHaven.DataServices.Implementation .ToListAsync(); } + public async Task> GetWorkOrderAuditLogsAsync(int workOrderId) + { + return await _context.WorkOrderAuditLogs + .Where(a => a.WorkOrderId == workOrderId) + .OrderByDescending(a => a.CreatedAt) + .ToListAsync(); + } + + public async Task AddWorkOrderContactsAsync(int workOrderId, IEnumerable 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 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 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> GetAllWithDetailsAsync() + { + return await _context.workOrders + .Where(w => w.istemplate != true) + .Include(w => w.Locations) + .Include(w => w.AssignToUser) + .AsSplitQuery() + .ToListAsync(); + } } } diff --git a/SeaHaven.DataServices/Interfaces/ICommentDataService.cs b/SeaHaven.DataServices/Interfaces/ICommentDataService.cs new file mode 100644 index 0000000..e6bd08c --- /dev/null +++ b/SeaHaven.DataServices/Interfaces/ICommentDataService.cs @@ -0,0 +1,16 @@ +using Data.SeaHavenIndustries; + +namespace SeaHaven.DataServices.Interfaces +{ + public interface ICommentDataService + { + Task GetByIdAsync(int id); + Task> GetAllAsync(); + Task> GetByWorkOrderIdAsync(int workOrderId); + Task> GetByDispatchIdAsync(int dispatchId); + Task AddAsync(Comments comment); + Task UpdateAsync(Comments comment); + Task DeleteAsync(int id); + Task ExistsAsync(int id); + } +} diff --git a/SeaHaven.DataServices/Interfaces/IDispatchDataService.cs b/SeaHaven.DataServices/Interfaces/IDispatchDataService.cs index 32ec4e0..52473ba 100644 --- a/SeaHaven.DataServices/Interfaces/IDispatchDataService.cs +++ b/SeaHaven.DataServices/Interfaces/IDispatchDataService.cs @@ -8,6 +8,8 @@ namespace SeaHaven.DataServices.Interfaces Task GetByIdWithDetailsAsync(int id); Task> GetAllAsync(); Task> GetByVendorIdAsync(int vendorId); + Task> GetByWorkOrderIdAsync(int workOrderId); + Task> GetByWorkOrderIdsAsync(IEnumerable workOrderIds); Task> GetByStatusAsync(string status); Task<(IEnumerable Items, int TotalCount)> GetPagedAsync(int page, int pageSize, string? search = null, string? status = null); Task AddAsync(Dispatch dispatch); @@ -15,6 +17,16 @@ namespace SeaHaven.DataServices.Interfaces Task DeleteAsync(int id); Task ExistsAsync(int id); Task GenerateNextDispatchNumberAsync(); + Task GenerateNextPONumberAsync(); Task CountAsync(); + Task> GetChecklistItemsAsync(int dispatchId); + Task GetChecklistItemByIdAsync(int id); + Task AddChecklistItemAsync(DispatchChecklistItem item); + Task UpdateChecklistItemAsync(DispatchChecklistItem item); + Task> GetSignoffsAsync(int dispatchId); + Task AddSignoffAsync(DispatchSignoff signoff); + Task HasSignoffTypeAsync(int dispatchId, string signoffType); + Task> GetDispatchWorkOrdersAsync(int dispatchId); + Task AddDispatchWorkOrderAsync(DispatchWorkOrder dispatchWorkOrder); } } diff --git a/SeaHaven.DataServices/Interfaces/IQuotesDataService.cs b/SeaHaven.DataServices/Interfaces/IQuotesDataService.cs new file mode 100644 index 0000000..8357876 --- /dev/null +++ b/SeaHaven.DataServices/Interfaces/IQuotesDataService.cs @@ -0,0 +1,16 @@ +using Data.SeaHavenIndustries; + +namespace SeaHaven.DataServices.Interfaces +{ + public interface IQuotesDataService + { + Task GetByIdAsync(int id); + Task> GetAllAsync(); + Task> GetByWorkOrderIdAsync(int workOrderId); + Task AddAsync(Quotes quote); + Task UpdateAsync(Quotes quote); + Task DeleteAsync(int id); + Task ExistsAsync(int id); + Task UnlinkFromWorkOrderAsync(int workOrderId); + } +} diff --git a/SeaHaven.DataServices/Interfaces/ITaskListTemplateDataService.cs b/SeaHaven.DataServices/Interfaces/ITaskListTemplateDataService.cs new file mode 100644 index 0000000..ac763d3 --- /dev/null +++ b/SeaHaven.DataServices/Interfaces/ITaskListTemplateDataService.cs @@ -0,0 +1,15 @@ +using Data.SeaHavenIndustries; + +namespace SeaHaven.DataServices.Interfaces +{ + public interface ITaskListTemplateDataService + { + Task GetByIdAsync(int id); + Task GetByIdWithItemsAsync(int id); + Task> GetAllAsync(); + Task AddAsync(TaskListTemplate template); + Task UpdateAsync(TaskListTemplate template); + Task DeleteAsync(int id); + Task ExistsAsync(int id); + } +} diff --git a/SeaHaven.DataServices/Interfaces/IUserDataService.cs b/SeaHaven.DataServices/Interfaces/IUserDataService.cs new file mode 100644 index 0000000..6524cc4 --- /dev/null +++ b/SeaHaven.DataServices/Interfaces/IUserDataService.cs @@ -0,0 +1,12 @@ +using Data.SeaHavenIndustries; + +namespace SeaHaven.DataServices.Interfaces +{ + public interface IUserDataService + { + Task GetByIdAsync(string id); + Task> GetAllAsync(); + Task GetByEmailAsync(string email); + Task ExistsAsync(string id); + } +} diff --git a/SeaHaven.DataServices/Interfaces/IWorkOrderDataService.cs b/SeaHaven.DataServices/Interfaces/IWorkOrderDataService.cs index 014c294..3ce62e5 100644 --- a/SeaHaven.DataServices/Interfaces/IWorkOrderDataService.cs +++ b/SeaHaven.DataServices/Interfaces/IWorkOrderDataService.cs @@ -38,22 +38,35 @@ namespace SeaHaven.DataServices.Interfaces Task UpdateAsync(WorkOrder workOrder); Task DeleteAsync(int id); - // Related entities + // Related entities - Read Task> GetWorkOrderContactsAsync(int workOrderId); Task> GetWorkOrderCategoriesAsync(int workOrderId); Task> GetWorkOrderAttachmentsAsync(int workOrderId); Task> GetWorkOrderCommentsAsync(int workOrderId); - + Task> GetWorkOrderAuditLogsAsync(int workOrderId); + + // Related entities - Write + Task AddWorkOrderContactsAsync(int workOrderId, IEnumerable contactIds); + Task AddWorkOrderCategoriesAsync(int workOrderId, IEnumerable 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 ExistsAsync(int id); Task InternalNumberExistsAsync(string internalNumber, int? excludeId = null); - + // Internal number generation Task GenerateNextInternalWONumberAsync(); - + // Statistics Task CountAsync(); Task CountByStatusAsync(string status); Task CountByLocationAsync(int locationId); + Task> GetAllWithDetailsAsync(); } } diff --git a/SeaHaven.Services/DTOs/ContactDTOs.cs b/SeaHaven.Services/DTOs/ContactDTOs.cs index ac7bb19..90cafc9 100644 --- a/SeaHaven.Services/DTOs/ContactDTOs.cs +++ b/SeaHaven.Services/DTOs/ContactDTOs.cs @@ -1,8 +1,5 @@ namespace SeaHaven.Services.DTOs { - /// - /// Contact DTO for responses - /// public class ContactDTO { public int Id { get; set; } @@ -21,9 +18,6 @@ namespace SeaHaven.Services.DTOs public string? ModifiedBy { get; set; } } - /// - /// DTO for creating a new contact - /// public class CreateContactDTO { public required string FirstName { get; set; } @@ -36,9 +30,6 @@ namespace SeaHaven.Services.DTOs public int? AccountId { get; set; } } - /// - /// DTO for updating an existing contact - /// public class UpdateContactDTO { public string? FirstName { get; set; } @@ -51,9 +42,6 @@ namespace SeaHaven.Services.DTOs public int? AccountId { get; set; } } - /// - /// Paged result wrapper - /// public class PagedResult { public IEnumerable Items { get; set; } = new List(); diff --git a/SeaHaven.Services/DTOs/WorkOrderDTOs.cs b/SeaHaven.Services/DTOs/WorkOrderDTOs.cs index c5f9a0f..ef73e7d 100644 --- a/SeaHaven.Services/DTOs/WorkOrderDTOs.cs +++ b/SeaHaven.Services/DTOs/WorkOrderDTOs.cs @@ -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; } + + /// + /// Maps entity to DTO (Entity -> DTO) + /// + 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(entity.Status, out var status) ? status : WorkOrderStatus.Open, + Priority = Enum.TryParse(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() + }; + } + + /// + /// Maps DTO to entity (DTO -> Entity) + /// + 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; + } } } diff --git a/SeaHaven.Services/DependencyInjection/ServicesModule.cs b/SeaHaven.Services/DependencyInjection/ServicesModule.cs new file mode 100644 index 0000000..12673b4 --- /dev/null +++ b/SeaHaven.Services/DependencyInjection/ServicesModule.cs @@ -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 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; + } + } +} diff --git a/SeaHaven.Services/Implementation/AccountService.cs b/SeaHaven.Services/Implementation/AccountService.cs index b059ea7..1b3223a 100644 --- a/SeaHaven.Services/Implementation/AccountService.cs +++ b/SeaHaven.Services/Implementation/AccountService.cs @@ -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 GetAccountByIdAsync(int id) @@ -50,6 +59,12 @@ namespace SeaHaven.Services.Implementation public async Task 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 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"); diff --git a/SeaHaven.Services/Implementation/AssetService.cs b/SeaHaven.Services/Implementation/AssetService.cs index d803227..4d5fcd3 100644 --- a/SeaHaven.Services/Implementation/AssetService.cs +++ b/SeaHaven.Services/Implementation/AssetService.cs @@ -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 GetAssetByIdAsync(int id) @@ -56,6 +65,12 @@ namespace SeaHaven.Services.Implementation public async Task 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 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"); diff --git a/SeaHaven.Services/Implementation/ContactService.cs b/SeaHaven.Services/Implementation/ContactService.cs index dcbcc35..cfeab5b 100644 --- a/SeaHaven.Services/Implementation/ContactService.cs +++ b/SeaHaven.Services/Implementation/ContactService.cs @@ -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 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 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); diff --git a/SeaHaven.Services/Implementation/DispatchService.cs b/SeaHaven.Services/Implementation/DispatchService.cs index 817a21f..2f2d200 100644 --- a/SeaHaven.Services/Implementation/DispatchService.cs +++ b/SeaHaven.Services/Implementation/DispatchService.cs @@ -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 GetDispatchByIdAsync(int id) @@ -56,6 +65,12 @@ namespace SeaHaven.Services.Implementation public async Task 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 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"); diff --git a/SeaHaven.Services/Implementation/EmployeeService.cs b/SeaHaven.Services/Implementation/EmployeeService.cs index 099c4a2..f8848a2 100644 --- a/SeaHaven.Services/Implementation/EmployeeService.cs +++ b/SeaHaven.Services/Implementation/EmployeeService.cs @@ -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 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 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"); diff --git a/SeaHaven.Services/Implementation/FollowUpService.cs b/SeaHaven.Services/Implementation/FollowUpService.cs index 9dce139..e95c517 100644 --- a/SeaHaven.Services/Implementation/FollowUpService.cs +++ b/SeaHaven.Services/Implementation/FollowUpService.cs @@ -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 GetFollowUpByIdAsync(int id) @@ -63,6 +72,12 @@ namespace SeaHaven.Services.Implementation public async Task 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 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"); diff --git a/SeaHaven.Services/Implementation/LocationService.cs b/SeaHaven.Services/Implementation/LocationService.cs index 67813f9..c625192 100644 --- a/SeaHaven.Services/Implementation/LocationService.cs +++ b/SeaHaven.Services/Implementation/LocationService.cs @@ -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 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 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"); diff --git a/SeaHaven.Services/Implementation/PMScheduleService.cs b/SeaHaven.Services/Implementation/PMScheduleService.cs index ab12d1e..1551a1a 100644 --- a/SeaHaven.Services/Implementation/PMScheduleService.cs +++ b/SeaHaven.Services/Implementation/PMScheduleService.cs @@ -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 GetPMScheduleByIdAsync(int id) @@ -56,6 +65,12 @@ namespace SeaHaven.Services.Implementation public async Task 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 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"); diff --git a/SeaHaven.Services/Implementation/VendorService.cs b/SeaHaven.Services/Implementation/VendorService.cs index 8bbe97c..eccc405 100644 --- a/SeaHaven.Services/Implementation/VendorService.cs +++ b/SeaHaven.Services/Implementation/VendorService.cs @@ -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 GetVendorByIdAsync(int id) @@ -50,6 +59,12 @@ namespace SeaHaven.Services.Implementation public async Task 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 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"); diff --git a/SeaHaven.Services/Implementation/WorkOrderService.cs b/SeaHaven.Services/Implementation/WorkOrderService.cs index 17b5f64..19fefc3 100644 --- a/SeaHaven.Services/Implementation/WorkOrderService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderService.cs @@ -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 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 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> GetAllWorkOrdersAsync() { var workOrders = await _workOrderDataService.GetAllAsync(); - return workOrders.Select(MapToDTO); + return workOrders.Select(WorkOrderDTO.MapToModel); } public async Task> 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 { - Items = items.Select(MapToDTO), + Items = items.Select(WorkOrderDTO.MapToModel), TotalCount = totalCount, Page = page, PageSize = pageSize }; } - - public async Task CreateWorkOrderAsync(CreateWorkOrderDTO dto, string userId) + public async Task 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 UpdateWorkOrderAsync(int id, UpdateWorkOrderDTO dto, string userId) + public async Task 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(); + 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 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 WorkOrderExistsAsync(int id) @@ -117,26 +129,40 @@ namespace SeaHaven.Services.Implementation return await _workOrderDataService.CountAsync(); } - private static WorkOrderDTO MapToDTO(WorkOrder workOrder) + private List TrackChanges(WorkOrder existing, UpdateWorkOrderDTO dto) { - return new WorkOrderDTO + var changes = new List(); + + 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); } } -} +} \ No newline at end of file diff --git a/SeaHaven.Services/Interfaces/IWorkOrderService.cs b/SeaHaven.Services/Interfaces/IWorkOrderService.cs index 4a34df3..913adcf 100644 --- a/SeaHaven.Services/Interfaces/IWorkOrderService.cs +++ b/SeaHaven.Services/Interfaces/IWorkOrderService.cs @@ -8,9 +8,9 @@ namespace SeaHaven.Services.Interfaces Task GetWorkOrderByIdWithDetailsAsync(int id); Task> GetAllWorkOrdersAsync(); Task> GetWorkOrdersPagedAsync(int page, int pageSize, string? search = null, string? status = null, string? priority = null); - Task CreateWorkOrderAsync(CreateWorkOrderDTO dto, string userId); - Task UpdateWorkOrderAsync(int id, UpdateWorkOrderDTO dto, string userId); - Task DeleteWorkOrderAsync(int id, string userId); + Task CreateWorkOrderAsync(CreateWorkOrderDTO dto, string userId); + Task UpdateWorkOrderAsync(int id, UpdateWorkOrderDTO dto, string userId); + Task DeleteWorkOrderAsync(int id, string userId); Task WorkOrderExistsAsync(int id); Task GenerateNextInternalWONumberAsync(); Task GetTotalWorkOrderCountAsync(); diff --git a/SeaHaven.Services/SeaHaven.Services.csproj b/SeaHaven.Services/SeaHaven.Services.csproj index e179849..281c47c 100644 --- a/SeaHaven.Services/SeaHaven.Services.csproj +++ b/SeaHaven.Services/SeaHaven.Services.csproj @@ -11,4 +11,9 @@ + + + + + diff --git a/SeaHaven.Services/Validation/AccountValidation.cs b/SeaHaven.Services/Validation/AccountValidation.cs new file mode 100644 index 0000000..fbbb465 --- /dev/null +++ b/SeaHaven.Services/Validation/AccountValidation.cs @@ -0,0 +1,57 @@ +using FluentValidation; +using SeaHaven.Services.DTOs; + +namespace SeaHaven.Services.Validation +{ + public interface ICreateAccountValidation : IValidator + { + } + + public class CreateAccountValidation : AbstractValidator, 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 + { + } + + public class UpdateAccountValidation : AbstractValidator, 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)); + } + } +} diff --git a/SeaHaven.Services/Validation/AssetValidation.cs b/SeaHaven.Services/Validation/AssetValidation.cs new file mode 100644 index 0000000..98b87d3 --- /dev/null +++ b/SeaHaven.Services/Validation/AssetValidation.cs @@ -0,0 +1,41 @@ +using FluentValidation; +using SeaHaven.Services.DTOs; + +namespace SeaHaven.Services.Validation +{ + public interface ICreateAssetValidation : IValidator + { + } + + public class CreateAssetValidation : AbstractValidator, 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 + { + } + + public class UpdateAssetValidation : AbstractValidator, 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); + } + } +} diff --git a/SeaHaven.Services/Validation/ContactValidation.cs b/SeaHaven.Services/Validation/ContactValidation.cs new file mode 100644 index 0000000..10411fc --- /dev/null +++ b/SeaHaven.Services/Validation/ContactValidation.cs @@ -0,0 +1,73 @@ +using FluentValidation; +using SeaHaven.Services.DTOs; + +namespace SeaHaven.Services.Validation +{ + public interface ICreateContactValidation : IValidator + { + } + + public class CreateContactValidation : AbstractValidator, 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 + { + } + + public class UpdateContactValidation : AbstractValidator, 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); + } + } +} diff --git a/SeaHaven.Services/Validation/DispatchValidation.cs b/SeaHaven.Services/Validation/DispatchValidation.cs new file mode 100644 index 0000000..c9806ec --- /dev/null +++ b/SeaHaven.Services/Validation/DispatchValidation.cs @@ -0,0 +1,44 @@ +using FluentValidation; +using SeaHaven.Services.DTOs; +using Data.SeaHavenIndustries.Enums; + +namespace SeaHaven.Services.Validation +{ + public interface ICreateDispatchValidation : IValidator + { + } + + public class CreateDispatchValidation : AbstractValidator, 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 + { + } + + public class UpdateDispatchValidation : AbstractValidator, 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"); + } + } +} diff --git a/SeaHaven.Services/Validation/EmployeeValidation.cs b/SeaHaven.Services/Validation/EmployeeValidation.cs new file mode 100644 index 0000000..19c9c39 --- /dev/null +++ b/SeaHaven.Services/Validation/EmployeeValidation.cs @@ -0,0 +1,81 @@ +using FluentValidation; +using SeaHaven.Services.DTOs; + +namespace SeaHaven.Services.Validation +{ + public interface ICreateEmployeeValidation : IValidator + { + } + + public class CreateEmployeeValidation : AbstractValidator, 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 + { + } + + public class UpdateEmployeeValidation : AbstractValidator, 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); + } + } +} diff --git a/SeaHaven.Services/Validation/FollowUpValidation.cs b/SeaHaven.Services/Validation/FollowUpValidation.cs new file mode 100644 index 0000000..dd01022 --- /dev/null +++ b/SeaHaven.Services/Validation/FollowUpValidation.cs @@ -0,0 +1,78 @@ +using FluentValidation; +using SeaHaven.Services.DTOs; +using Data.SeaHavenIndustries.Enums; + +namespace SeaHaven.Services.Validation +{ + public interface ICreateFollowUpValidation : IValidator + { + } + + public class CreateFollowUpValidation : AbstractValidator, 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 + { + } + + public class UpdateFollowUpValidation : AbstractValidator, 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); + } + } +} diff --git a/SeaHaven.Services/Validation/LocationValidation.cs b/SeaHaven.Services/Validation/LocationValidation.cs new file mode 100644 index 0000000..4c9cc97 --- /dev/null +++ b/SeaHaven.Services/Validation/LocationValidation.cs @@ -0,0 +1,73 @@ +using FluentValidation; +using SeaHaven.Services.DTOs; + +namespace SeaHaven.Services.Validation +{ + public interface ICreateLocationValidation : IValidator + { + } + + public class CreateLocationValidation : AbstractValidator, 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 + { + } + + public class UpdateLocationValidation : AbstractValidator, 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); + } + } +} diff --git a/SeaHaven.Services/Validation/PMScheduleValidation.cs b/SeaHaven.Services/Validation/PMScheduleValidation.cs new file mode 100644 index 0000000..48a2f84 --- /dev/null +++ b/SeaHaven.Services/Validation/PMScheduleValidation.cs @@ -0,0 +1,62 @@ +using FluentValidation; +using SeaHaven.Services.DTOs; + +namespace SeaHaven.Services.Validation +{ + public interface ICreatePMScheduleValidation : IValidator + { + } + + public class CreatePMScheduleValidation : AbstractValidator, 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 + { + } + + public class UpdatePMScheduleValidation : AbstractValidator, 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); + } + } +} diff --git a/SeaHaven.Services/Validation/VendorValidation.cs b/SeaHaven.Services/Validation/VendorValidation.cs new file mode 100644 index 0000000..b2f5c03 --- /dev/null +++ b/SeaHaven.Services/Validation/VendorValidation.cs @@ -0,0 +1,81 @@ +using FluentValidation; +using SeaHaven.Services.DTOs; + +namespace SeaHaven.Services.Validation +{ + public interface ICreateVendorValidation : IValidator + { + } + + public class CreateVendorValidation : AbstractValidator, 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 + { + } + + public class UpdateVendorValidation : AbstractValidator, 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)); + } + } +} diff --git a/SeaHaven.Services/Validation/WorkOrderValidation.cs b/SeaHaven.Services/Validation/WorkOrderValidation.cs new file mode 100644 index 0000000..5baeb68 --- /dev/null +++ b/SeaHaven.Services/Validation/WorkOrderValidation.cs @@ -0,0 +1,71 @@ +using FluentValidation; +using SeaHaven.Services.DTOs; +using Data.SeaHavenIndustries.Enums; + +namespace SeaHaven.Services.Validation +{ + public interface ICreateWorkOrderValidation : IValidator + { + } + + public class CreateWorkOrderValidation : AbstractValidator, 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 + { + } + + public class UpdateWorkOrderValidation : AbstractValidator, 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); + } + } +}