mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 08:23:12 +00:00
backend changes
This commit is contained in:
parent
5e4d9894e9
commit
59385cf5b1
54 changed files with 4134 additions and 731 deletions
344
ARCHITECTURE_AT_A_GLANCE.md
Normal file
344
ARCHITECTURE_AT_A_GLANCE.md
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
# Architecture At A Glance
|
||||
|
||||
## 📐 The 4-Layer Architecture
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ 🌐 API LAYER (Controllers) │
|
||||
│ - HTTP Endpoints │
|
||||
│ - Authentication │
|
||||
│ - Exception → HTTP Status Code mapping │
|
||||
│ ❌ NO Business Logic │
|
||||
│ ❌ NO Database Access │
|
||||
└─────────────────┬────────────────────────────────────┘
|
||||
│
|
||||
│ Calls
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ 💼 SERVICE LAYER (Business Logic) │
|
||||
│ - FluentValidation │
|
||||
│ - Business Rules │
|
||||
│ - Orchestration │
|
||||
│ - DTO Transformations │
|
||||
│ ❌ NO HTTP Concerns │
|
||||
│ ❌ NO Database Access │
|
||||
└─────────────────┬────────────────────────────────────┘
|
||||
│
|
||||
│ Calls
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ 💾 DATA SERVICE LAYER (Persistence) │
|
||||
│ - CRUD Operations │
|
||||
│ - Database Queries │
|
||||
│ - Audit Field Management │
|
||||
│ ❌ NO Validation │
|
||||
│ ❌ NO Business Logic │
|
||||
└─────────────────┬────────────────────────────────────┘
|
||||
│
|
||||
│ Uses
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ 🗄️ DATA LAYER (EF Core + Entities) │
|
||||
│ - DbContext │
|
||||
│ - Entity Models (POCOs) │
|
||||
│ - Migrations │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Responsibilities Matrix
|
||||
|
||||
| Layer | What It Does | What It Doesn't Do |
|
||||
|-------|--------------|-------------------|
|
||||
| **Controller** | • Handle HTTP requests<br>• Authenticate users<br>• Map API DTOs<br>• Catch exceptions<br>• Return HTTP status codes | • Business logic<br>• Validation rules<br>• Database queries |
|
||||
| **Service** | • Validate data (FluentValidation)<br>• Apply business rules<br>• Coordinate operations<br>• Transform data | • HTTP concerns<br>• Database operations |
|
||||
| **DataService** | • Execute database queries<br>• CRUD operations<br>• Set audit fields | • Validation<br>• Business rules |
|
||||
| **Data** | • Define entities<br>• Database schema | • Application logic |
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Request Flow
|
||||
|
||||
### Creating a Contact (Example)
|
||||
|
||||
```
|
||||
1. HTTP POST /api/Contact/Create
|
||||
↓
|
||||
2. ContactController.Create()
|
||||
- Checks authentication
|
||||
- Gets userId from claims
|
||||
↓
|
||||
3. ContactService.CreateContactAsync()
|
||||
- Validates using FluentValidation
|
||||
- Applies business rules
|
||||
↓
|
||||
4. ContactDataService.CreateAsync()
|
||||
- Creates entity
|
||||
- Sets audit fields
|
||||
- Saves to database
|
||||
↓
|
||||
5. Returns through layers:
|
||||
Entity → DataDTO → ServiceDTO → ApiDTO → HTTP Response
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 DTO Flow
|
||||
|
||||
```
|
||||
Frontend (JSON)
|
||||
↓
|
||||
API DTO (Contact_DTO) ← Controller receives this
|
||||
↓
|
||||
Service DTO (CreateContactDTO) ← Service validates this
|
||||
↓
|
||||
Data DTO (CreateContactDataDTO) ← DataService uses this
|
||||
↓
|
||||
Entity (Contact) ← Database stores this
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔌 Dependency Injection
|
||||
|
||||
### Automatic Assembly Scanning
|
||||
|
||||
Services auto-register via assembly scanning (no manual registration needed).
|
||||
|
||||
```csharp
|
||||
// Program.cs
|
||||
builder.Services.AddDataServices(); // Auto-register data services
|
||||
builder.Services.AddBusinessServices(); // Auto-register business services + validators
|
||||
```
|
||||
|
||||
### Naming Convention (REQUIRED)
|
||||
|
||||
| Interface | Implementation | Registered? |
|
||||
|-----------|---------------|-------------|
|
||||
| `IContactService` | `ContactService` | ✅ YES |
|
||||
| `ISomething` | `SomethingElse` | ❌ NO |
|
||||
|
||||
**Rule**: Interface = `I` + Class name exactly
|
||||
|
||||
---
|
||||
|
||||
## ✅ Validation
|
||||
|
||||
### Where and How
|
||||
|
||||
```
|
||||
❌ Controller → Catches ValidationException
|
||||
✅ Service → Runs FluentValidation.Validate()
|
||||
❌ DataService → No validation
|
||||
```
|
||||
|
||||
### FluentValidation Example
|
||||
|
||||
```csharp
|
||||
public class CreateContactDTOValidator : AbstractValidator<CreateContactDTO>
|
||||
{
|
||||
public CreateContactDTOValidator()
|
||||
{
|
||||
RuleFor(x => x.FirstName)
|
||||
.NotEmpty().WithMessage("First name is required")
|
||||
.MaximumLength(50);
|
||||
|
||||
RuleFor(x => x.Email)
|
||||
.EmailAddress().WithMessage("Invalid email");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### In Service
|
||||
|
||||
```csharp
|
||||
var validationResult = await _validator.ValidateAsync(dto);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ValidationException(validationResult.Errors);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚨 Exception Handling
|
||||
|
||||
### Standard Pattern (All Controllers)
|
||||
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (userId == null)
|
||||
return Unauthorized(...);
|
||||
|
||||
await _service.SomeMethod(dto, userId);
|
||||
return Ok(...);
|
||||
}
|
||||
catch (ValidationException vex) // 400 Bad Request
|
||||
{
|
||||
return BadRequest(...);
|
||||
}
|
||||
catch (KeyNotFoundException) // 404 Not Found
|
||||
{
|
||||
return NotFound(...);
|
||||
}
|
||||
catch (Exception ex) // 500 Internal Server Error
|
||||
{
|
||||
return StatusCode(500, ...);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Authentication
|
||||
|
||||
### Getting User ID
|
||||
|
||||
```csharp
|
||||
// Method 1: Claims (Preferred)
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
|
||||
// Method 2: UserManager (for Identity)
|
||||
var userId = _userManager.GetUserId(User);
|
||||
|
||||
// Always check for null
|
||||
if (userId == null)
|
||||
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
||||
```
|
||||
|
||||
### Audit Fields
|
||||
|
||||
Every entity has:
|
||||
```csharp
|
||||
public string CreatedBy { get; set; } // User ID (GUID)
|
||||
public DateTime CreatedDate { get; set; } // UTC timestamp
|
||||
public string ModifiedBy { get; set; } // User ID (GUID)
|
||||
public DateTime ModifiedDate { get; set; } // UTC timestamp
|
||||
```
|
||||
|
||||
Set in DataService:
|
||||
```csharp
|
||||
CreatedBy = userId,
|
||||
CreatedDate = DateTime.UtcNow,
|
||||
ModifiedBy = userId,
|
||||
ModifiedDate = DateTime.UtcNow
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Naming Conventions
|
||||
|
||||
| Component | Pattern | Example |
|
||||
|-----------|---------|---------|
|
||||
| Entity | `[Entity]` | `Contact` |
|
||||
| Controller | `[Entity]Controller` | `ContactController` |
|
||||
| Service Interface | `I[Entity]Service` | `IContactService` |
|
||||
| Service Class | `[Entity]Service` | `ContactService` |
|
||||
| DataService Interface | `I[Entity]DataService` | `IContactDataService` |
|
||||
| DataService Class | `[Entity]DataService` | `ContactDataService` |
|
||||
| API DTO | `[Entity]_DTO` | `Contact_DTO` |
|
||||
| Service DTO | `[Action][Entity]DTO` | `CreateContactDTO` |
|
||||
| Data DTO | `[Action][Entity]DataDTO` | `CreateContactDataDTO` |
|
||||
| Validator | `[DTO]Validator` | `CreateContactDTOValidator` |
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Code Templates
|
||||
|
||||
### Controller Method Template
|
||||
```csharp
|
||||
[HttpPost("ActionName")]
|
||||
public async Task<IActionResult> ActionName([FromBody] SomeDTO dto)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (userId == null)
|
||||
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
||||
|
||||
await _service.Method(dto, userId);
|
||||
|
||||
return Ok(new Response { Status = "Success", Message = "Success message" });
|
||||
}
|
||||
catch (ValidationException vex)
|
||||
{
|
||||
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
|
||||
return BadRequest(new Response { Status = "Validation Error", Message = errors });
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
return NotFound(new Response { Status = "Error", Message = "Not found" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Service Method Template
|
||||
```csharp
|
||||
public async Task<ResultDTO> MethodAsync(InputDTO dto, string userId)
|
||||
{
|
||||
// 1. Validate
|
||||
var validationResult = await _validator.ValidateAsync(dto);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ValidationException(validationResult.Errors);
|
||||
}
|
||||
|
||||
// 2. Business logic (if any)
|
||||
|
||||
// 3. Call DataService
|
||||
var result = await _dataService.MethodAsync(dataDto, userId);
|
||||
|
||||
// 4. Map and return
|
||||
return MapToServiceDTO(result);
|
||||
}
|
||||
```
|
||||
|
||||
### DataService Method Template
|
||||
```csharp
|
||||
public async Task<DataDTO> MethodAsync(InputDataDTO dto, string userId)
|
||||
{
|
||||
// 1. Create/modify entity
|
||||
var entity = new Entity
|
||||
{
|
||||
Property = dto.Property,
|
||||
CreatedBy = userId,
|
||||
CreatedDate = DateTime.UtcNow,
|
||||
ModifiedBy = userId,
|
||||
ModifiedDate = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// 2. Database operation
|
||||
_db.Entities.Add(entity);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
// 3. Map and return
|
||||
return MapToDataDTO(entity);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Key Rules
|
||||
|
||||
1. **Never skip layers** - Always go Controller → Service → DataService
|
||||
2. **No direct `_db` in Controllers** - Database access only in DataService
|
||||
3. **Validate in Service** - Use FluentValidation, not manual checks
|
||||
4. **Always check userId** - Return 401 if null
|
||||
5. **Catch ValidationException first** - Before generic Exception
|
||||
6. **Use interfaces everywhere** - For dependency injection
|
||||
7. **Set audit fields** - CreatedBy, CreatedDate, ModifiedBy, ModifiedDate
|
||||
8. **Return proper HTTP codes** - 200, 400, 401, 404, 500
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Learn More
|
||||
|
||||
- **Full Details**: See `BACKEND_ARCHITECTURE.md`
|
||||
- **Step-by-Step Guide**: See `QUICK_START_GUIDE.md`
|
||||
- **Working Examples**: Look at `ContactController`, `ContactService`, `ContactDataService`
|
||||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using Api.SeaHavenIndustries.DTOs;
|
||||
using Data.SeaHavenIndustries;
|
||||
using FluentValidation;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
|
@ -34,15 +35,22 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
{
|
||||
try
|
||||
{
|
||||
// ✅ Use DTO mapping method instead of manual property assignment
|
||||
var userId = _userManager.GetUserId(User) ?? "system";
|
||||
var userId = _userManager.GetUserId(User);
|
||||
if (userId == null)
|
||||
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
||||
|
||||
await _contactService.CreateContactAsync(contacts.ToServiceCreateDTO(), userId);
|
||||
|
||||
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
|
||||
return Ok(new DataResponse { Message = "Contact created successfully", Status = "200" });
|
||||
}
|
||||
catch (ValidationException vex)
|
||||
{
|
||||
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
|
||||
return BadRequest(new Response { Status = "Validation Error", Message = errors });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||||
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -52,19 +60,26 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
{
|
||||
try
|
||||
{
|
||||
// ✅ Use DTO mapping method instead of manual property assignment
|
||||
var userId = _userManager.GetUserId(User) ?? "system";
|
||||
var userId = _userManager.GetUserId(User);
|
||||
if (userId == null)
|
||||
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
||||
|
||||
await _contactService.UpdateContactAsync(contacts.Id, contacts.ToServiceUpdateDTO(), userId);
|
||||
|
||||
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
|
||||
return Ok(new DataResponse { Message = "Contact updated successfully", Status = "200" });
|
||||
}
|
||||
catch (ValidationException vex)
|
||||
{
|
||||
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
|
||||
return BadRequest(new Response { Status = "Validation Error", Message = errors });
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
return BadRequest(new Response { Status = "Error", Message = "Contact not found" });
|
||||
return NotFound(new Response { Status = "Error", Message = "Contact not found" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||||
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -74,30 +89,28 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
{
|
||||
try
|
||||
{
|
||||
var userId = _userManager.GetUserId(User) ?? "system";
|
||||
var userId = _userManager.GetUserId(User);
|
||||
if (userId == null)
|
||||
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
||||
|
||||
// ✅ Call Service layer (not DataService directly)
|
||||
await _contactService.DeleteContactAsync(id, userId);
|
||||
|
||||
return Ok(new DataResponse { Message = "Removed Successfully", Status = "200" });
|
||||
return Ok(new DataResponse { Message = "Contact deleted successfully", Status = "200" });
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
return BadRequest(new Response { Status = "Error", Message = "ID not matched!" });
|
||||
return NotFound(new Response { Status = "Error", Message = "Contact not found" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||||
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
[HttpGet]
|
||||
[Route("GetContactList")]
|
||||
public async Task<IActionResult> GetContactList(string? search ="", int page = 1, int pageSize = 10)
|
||||
{
|
||||
// ✅ Call Service layer
|
||||
var pagedResult = await _contactService.GetContactsPagedAsync(page, pageSize, search);
|
||||
|
||||
// ✅ Use DTO mapping method (NO manual transformation in controller)
|
||||
var data = ContactListItem_DTO.FromServiceDTOs(pagedResult.Items);
|
||||
var totalPages = (int)Math.Ceiling(pagedResult.TotalCount / (double)pageSize);
|
||||
|
||||
|
|
@ -116,7 +129,6 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
[Route("GetContacts")]
|
||||
public async Task<IActionResult> GetContacts()
|
||||
{
|
||||
// ✅ Call Service layer
|
||||
var data = await _contactService.GetAllContactsAsync();
|
||||
return Ok(data);
|
||||
}
|
||||
|
|
@ -124,24 +136,15 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
[Route("GetLocations")]
|
||||
public async Task<IActionResult> GetLocations()
|
||||
{
|
||||
// ✅ Call Service layer
|
||||
var locations = await _locationService.GetAllLocationsAsync();
|
||||
|
||||
// ✅ Use DTO mapping method (NO manual transformation in controller)
|
||||
var data = LocationViewModel_DTO.FromServiceDTOs(locations);
|
||||
return Ok(data);
|
||||
}
|
||||
|
||||
// TODO: ARCHITECTURAL VIOLATION - These methods should NOT be in ContactController
|
||||
// GetCategories should be in a CategoryController with ICategoryService
|
||||
// GetUsers should be in a UserController with IUserService
|
||||
// For now, commenting out to prevent direct database access
|
||||
|
||||
[HttpGet]
|
||||
[Route("GetCategories")]
|
||||
public async Task<IActionResult> GetCategories()
|
||||
{
|
||||
// ❌ TEMPORARY: Needs ICategoryService
|
||||
return StatusCode(501, new Response { Status = "Error", Message = "Not Implemented - Needs CategoryService" });
|
||||
}
|
||||
|
||||
|
|
@ -149,11 +152,8 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
[Route("GetUsers")]
|
||||
public async Task<IActionResult> GetUsers()
|
||||
{
|
||||
// ❌ TEMPORARY: Needs IUserService
|
||||
return StatusCode(501, new Response { Status = "Error", Message = "Not Implemented - Needs UserService" });
|
||||
}
|
||||
// TODO: ARCHITECTURAL VIOLATION - Location CRUD methods should NOT be in ContactController
|
||||
// These should be in LocationController with ILocationService
|
||||
|
||||
[HttpPost]
|
||||
[Route("AddLocation")]
|
||||
|
|
@ -161,7 +161,6 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
{
|
||||
try
|
||||
{
|
||||
// ✅ Use Service layer with DTO mapping
|
||||
var userId = _userManager.GetUserId(User) ?? "system";
|
||||
await _locationService.CreateLocationAsync(model.ToServiceCreateDTO(), userId);
|
||||
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
|
||||
|
|
@ -178,7 +177,6 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
{
|
||||
try
|
||||
{
|
||||
// ✅ Use Service layer with DTO mapping
|
||||
var userId = _userManager.GetUserId(User) ?? "system";
|
||||
await _locationService.UpdateLocationAsync(model.Id, model.ToServiceUpdateDTO(), userId);
|
||||
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
|
||||
|
|
@ -199,7 +197,6 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
{
|
||||
try
|
||||
{
|
||||
// ✅ Use Service layer
|
||||
var userId = _userManager.GetUserId(User) ?? "system";
|
||||
await _locationService.DeleteLocationAsync(Id, userId);
|
||||
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
|
||||
|
|
@ -218,10 +215,7 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
[Route("GetAddressbook")]
|
||||
public async Task<IActionResult> GetAddressbook(string? search = "", int page = 1, int pageSize = 10)
|
||||
{
|
||||
// ✅ Call Service layer
|
||||
var pagedResult = await _locationService.GetLocationsPagedAsync(page, pageSize, search);
|
||||
|
||||
// ✅ Use DTO mapping method (NO manual transformation in controller)
|
||||
var data = LocationListItem_DTO.FromServiceDTOs(pagedResult.Items);
|
||||
var totalPages = (int)Math.Ceiling(pagedResult.TotalCount / (double)pageSize);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
using Api.SeaHavenIndustries.DTOs;
|
||||
using Data.SeaHavenIndustries;
|
||||
using FluentValidation;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using System.Security.Claims;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/Employee")]
|
||||
|
|
@ -18,9 +20,10 @@ public class EmployeeController : Controller
|
|||
}
|
||||
|
||||
// -----------------------------------
|
||||
// POST: api/Employee/AddEmployees
|
||||
// POST: api/Employee/AddEmployees or /Create
|
||||
// -----------------------------------
|
||||
[HttpPost("AddEmployees")]
|
||||
[HttpPost("Create")]
|
||||
public async Task<IActionResult> AddEmployees(AddEmployee_DTO dto)
|
||||
{
|
||||
try
|
||||
|
|
@ -35,21 +38,30 @@ public class EmployeeController : Controller
|
|||
Phone = dto.Phone
|
||||
};
|
||||
|
||||
var userId = User.Identity?.Name ?? "system";
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (userId == null)
|
||||
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
||||
|
||||
await _employeeService.CreateEmployeeAsync(createDto, userId);
|
||||
|
||||
return Ok(new { Message = "Employee added successfully" });
|
||||
}
|
||||
catch (ValidationException vex)
|
||||
{
|
||||
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
|
||||
return BadRequest(new Response { Status = "Validation Error", Message = errors });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new { Message = ex.Message });
|
||||
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------
|
||||
// POST: api/Employee/EditEmployees
|
||||
// POST: api/Employee/EditEmployees or /Update
|
||||
// -----------------------------------
|
||||
[HttpPost("EditEmployees")]
|
||||
[HttpPost("Update")]
|
||||
public async Task<IActionResult> EditEmployees(EditEmployee_DTO dto)
|
||||
{
|
||||
try
|
||||
|
|
@ -61,14 +73,26 @@ public class EmployeeController : Controller
|
|||
JobTitleId = dto.JobTitleId
|
||||
};
|
||||
|
||||
var userId = User.Identity?.Name ?? "system";
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (userId == null)
|
||||
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
||||
|
||||
await _employeeService.UpdateEmployeeAsync(dto.Id, updateDto, userId);
|
||||
|
||||
return Ok(new { Message = "Employee updated successfully" });
|
||||
}
|
||||
catch (ValidationException vex)
|
||||
{
|
||||
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
|
||||
return BadRequest(new Response { Status = "Validation Error", Message = errors });
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
return NotFound(new Response { Status = "Error", Message = "Employee not found" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new { Message = ex.Message });
|
||||
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -106,14 +130,50 @@ public class EmployeeController : Controller
|
|||
return Ok(viewModel);
|
||||
}
|
||||
// -----------------------------------
|
||||
// GET: api/Employee/DeleteEmployees?id=5
|
||||
// GET: api/Employee/GetById?id=5
|
||||
// -----------------------------------
|
||||
[HttpGet("GetById")]
|
||||
public async Task<IActionResult> GetById(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var employee = await _employeeService.GetEmployeeByIdAsync(id);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
id = employee.Id,
|
||||
firstName = employee.FirstName,
|
||||
lastName = employee.LastName,
|
||||
jobTitle = employee.JobTitleName ?? "",
|
||||
email = employee.Email ?? "",
|
||||
phone = employee.Phone ?? "",
|
||||
department = employee.DepartmentName ?? "",
|
||||
status = "Active"
|
||||
});
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
return NotFound(new Response { Status = "Error", Message = "Employee not found" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------
|
||||
// GET: api/Employee/DeleteEmployees or /Delete?id=5
|
||||
// -----------------------------------
|
||||
[HttpGet("DeleteEmployees")]
|
||||
[HttpPost("Delete")]
|
||||
public async Task<IActionResult> DeleteEmployees(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userId = User.Identity?.Name ?? "system";
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (userId == null)
|
||||
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
||||
|
||||
await _employeeService.DeleteEmployeeAsync(id, userId);
|
||||
|
||||
return Ok(new DataResponse
|
||||
|
|
@ -130,14 +190,31 @@ public class EmployeeController : Controller
|
|||
Message = ex.Message
|
||||
});
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
return NotFound(new Response { Status = "Error", Message = "Employee not found" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new Response
|
||||
{
|
||||
Status = "Error",
|
||||
Message = ex.Message
|
||||
});
|
||||
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------
|
||||
// GET: api/Employee/Dropdown
|
||||
// -----------------------------------
|
||||
[HttpGet("Dropdown")]
|
||||
public async Task<IActionResult> GetDropdown()
|
||||
{
|
||||
var pagedResult = await _employeeService.GetEmployeesPagedAsync(1, 1000, "");
|
||||
|
||||
var data = pagedResult.Items.Select(e => new
|
||||
{
|
||||
id = e.Id,
|
||||
name = $"{e.FirstName} {e.LastName}".Trim()
|
||||
});
|
||||
|
||||
return Ok(data);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Security.Claims;
|
||||
using Api.SeaHavenIndustries.DTOs;
|
||||
using Data.SeaHavenIndustries;
|
||||
using FluentValidation;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
|
@ -157,44 +158,75 @@ public class FollowUpController : ControllerBase
|
|||
[HttpPost("Create")]
|
||||
public async Task<IActionResult> Create(FollowUp_DTO dto)
|
||||
{
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
|
||||
var model = new FollowUps
|
||||
try
|
||||
{
|
||||
Reason = dto.Reason,
|
||||
ScheduleDate = dto.ScheduleDate,
|
||||
ScheduleStartTime = dto.ScheduleStartTime,
|
||||
EmployeeId = dto.EmployeeId,
|
||||
WorkOrderId = dto.WorkOrderId,
|
||||
AccountId = dto.AccountId,
|
||||
LocationId = dto.LocationId,
|
||||
Status = "Open",
|
||||
CreatedDate = DateTime.UtcNow,
|
||||
createdby = userId
|
||||
};
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (userId == null)
|
||||
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
||||
|
||||
await _followUpDataService.AddAsync(model);
|
||||
var model = new FollowUps
|
||||
{
|
||||
Reason = dto.Reason,
|
||||
ScheduleDate = dto.ScheduleDate,
|
||||
ScheduleStartTime = dto.ScheduleStartTime,
|
||||
EmployeeId = dto.EmployeeId,
|
||||
WorkOrderId = dto.WorkOrderId,
|
||||
AccountId = dto.AccountId,
|
||||
LocationId = dto.LocationId,
|
||||
Status = "Open",
|
||||
CreatedDate = DateTime.UtcNow,
|
||||
createdby = userId
|
||||
};
|
||||
|
||||
return Ok(new DataResponse { Status = "200", Message = "Follow Up created", Data = model.Id });
|
||||
await _followUpDataService.AddAsync(model);
|
||||
|
||||
return Ok(new DataResponse { Status = "200", Message = "Follow Up created", Data = model.Id });
|
||||
}
|
||||
catch (ValidationException vex)
|
||||
{
|
||||
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
|
||||
return BadRequest(new Response { Status = "Validation Error", Message = errors });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("Update")]
|
||||
public async Task<IActionResult> Update(FollowUp_DTO dto)
|
||||
{
|
||||
var model = await _followUpDataService.GetByIdAsync(dto.Id);
|
||||
if (model == null) return NotFound();
|
||||
try
|
||||
{
|
||||
var model = await _followUpDataService.GetByIdAsync(dto.Id);
|
||||
if (model == null)
|
||||
return NotFound(new Response { Status = "Error", Message = "Follow Up not found" });
|
||||
|
||||
model.Reason = dto.Reason;
|
||||
model.ScheduleDate = dto.ScheduleDate;
|
||||
model.ScheduleStartTime = dto.ScheduleStartTime;
|
||||
model.EmployeeId = dto.EmployeeId;
|
||||
model.WorkOrderId = dto.WorkOrderId;
|
||||
model.AccountId = dto.AccountId;
|
||||
model.LocationId = dto.LocationId;
|
||||
model.LastModificationTime = DateTime.UtcNow;
|
||||
model.Reason = dto.Reason;
|
||||
model.ScheduleDate = dto.ScheduleDate;
|
||||
model.ScheduleStartTime = dto.ScheduleStartTime;
|
||||
model.EmployeeId = dto.EmployeeId;
|
||||
model.WorkOrderId = dto.WorkOrderId;
|
||||
model.AccountId = dto.AccountId;
|
||||
model.LocationId = dto.LocationId;
|
||||
model.LastModificationTime = DateTime.UtcNow;
|
||||
|
||||
await _followUpDataService.UpdateAsync(model);
|
||||
return Ok(new DataResponse { Status = "200", Message = "Follow Up updated" });
|
||||
await _followUpDataService.UpdateAsync(model);
|
||||
return Ok(new DataResponse { Status = "200", Message = "Follow Up updated" });
|
||||
}
|
||||
catch (ValidationException vex)
|
||||
{
|
||||
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
|
||||
return BadRequest(new Response { Status = "Validation Error", Message = errors });
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
return NotFound(new Response { Status = "Error", Message = "Follow Up not found" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("Complete")]
|
||||
|
|
@ -226,11 +258,23 @@ public class FollowUpController : ControllerBase
|
|||
[HttpPost("Delete")]
|
||||
public async Task<IActionResult> Delete(int id)
|
||||
{
|
||||
var model = await _followUpDataService.GetByIdAsync(id);
|
||||
if (model == null) return NotFound();
|
||||
try
|
||||
{
|
||||
var model = await _followUpDataService.GetByIdAsync(id);
|
||||
if (model == null)
|
||||
return NotFound(new Response { Status = "Error", Message = "Follow Up not found" });
|
||||
|
||||
await _followUpDataService.DeleteAsync(id);
|
||||
await _followUpDataService.DeleteAsync(id);
|
||||
|
||||
return Ok(new DataResponse { Status = "200", Message = "Follow Up deleted" });
|
||||
return Ok(new DataResponse { Status = "200", Message = "Follow Up deleted" });
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
return NotFound(new Response { Status = "Error", Message = "Follow Up not found" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
using Api.SeaHavenIndustries.DTOs;
|
||||
using Data.SeaHavenIndustries;
|
||||
using FluentValidation;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using System.Security.Claims;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/PMSchedule")]
|
||||
|
|
@ -36,12 +38,46 @@ public class PMScheduleController : ControllerBase
|
|||
return Ok(data);
|
||||
}
|
||||
|
||||
[HttpGet("GetById")]
|
||||
public async Task<IActionResult> GetById([FromQuery] int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var pmSchedule = await _pmScheduleService.GetPMScheduleByIdAsync(id);
|
||||
if (pmSchedule == null)
|
||||
return NotFound(new Response { Status = "Error", Message = "PM Schedule not found" });
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Id = pmSchedule.Id,
|
||||
Name = pmSchedule.Name,
|
||||
Frequency = pmSchedule.Frequency,
|
||||
StartDate = pmSchedule.StartDate,
|
||||
IsActive = pmSchedule.IsActive,
|
||||
AssetId = pmSchedule.AssetId,
|
||||
LocationId = pmSchedule.LocationId,
|
||||
Asset = pmSchedule.AssetName,
|
||||
Location = pmSchedule.LocationName
|
||||
});
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
return NotFound(new Response { Status = "Error", Message = "PM Schedule not found" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("Save")]
|
||||
public async Task<IActionResult> Save(PMSchedule_DTO dto)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userId = User.Identity?.Name ?? "system";
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (userId == null)
|
||||
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
||||
|
||||
if (dto.Id == 0)
|
||||
{
|
||||
|
|
@ -72,9 +108,40 @@ public class PMScheduleController : ControllerBase
|
|||
|
||||
return Ok();
|
||||
}
|
||||
catch (ValidationException vex)
|
||||
{
|
||||
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
|
||||
return BadRequest(new Response { Status = "Validation Error", Message = errors });
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
return NotFound(new Response { Status = "Error", Message = "PM Schedule not found" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new { Message = ex.Message });
|
||||
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("Delete")]
|
||||
public async Task<IActionResult> Delete([FromQuery] int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (userId == null)
|
||||
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
||||
|
||||
await _pmScheduleService.DeletePMScheduleAsync(id, userId);
|
||||
return Ok(new DataResponse { Message = "PM Schedule deleted successfully", Status = "200" });
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
return NotFound(new Response { Status = "Error", Message = "PM Schedule not found" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
using Api.SeaHavenIndustries.DTOs;
|
||||
using Data.SeaHavenIndustries;
|
||||
using FluentValidation;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Api.SeaHavenIndustries.Controllers
|
||||
{
|
||||
|
|
@ -53,9 +55,13 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<IActionResult> GetVendorById(int id)
|
||||
[HttpGet("GetById")]
|
||||
public async Task<IActionResult> GetVendorById([FromQuery] int? id)
|
||||
{
|
||||
var vendor = await _vendorService.GetVendorByIdAsync(id);
|
||||
if (id == null)
|
||||
return BadRequest(new Response { Status = "Error", Message = "Id is required" });
|
||||
|
||||
var vendor = await _vendorService.GetVendorByIdAsync(id.Value);
|
||||
|
||||
if (vendor == null)
|
||||
return NotFound(new Response { Status = "Error", Message = "Vendor not found" });
|
||||
|
|
@ -64,6 +70,7 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
}
|
||||
|
||||
[HttpPost]
|
||||
[HttpPost("Create")]
|
||||
public async Task<IActionResult> Create([FromBody] Vendor_DTO model)
|
||||
{
|
||||
try
|
||||
|
|
@ -73,42 +80,88 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
Name = model.CompanyName ?? throw new ArgumentException("CompanyName is required")
|
||||
};
|
||||
|
||||
var userId = User.Identity?.Name ?? "system";
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (userId == null)
|
||||
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
||||
|
||||
await _vendorService.CreateVendorAsync(createDto, userId);
|
||||
return Ok(new DataResponse { Message = "Vendor Created", Status = "200" });
|
||||
}
|
||||
catch (ValidationException vex)
|
||||
{
|
||||
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
|
||||
return BadRequest(new Response { Status = "Validation Error", Message = errors });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||||
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public async Task<IActionResult> Update(int id, [FromBody] EditVendor_DTO model)
|
||||
[HttpPost("Update")]
|
||||
public async Task<IActionResult> Update([FromRoute] int? id, [FromBody] EditVendor_DTO model)
|
||||
{
|
||||
try
|
||||
{
|
||||
// For named route, id comes from model
|
||||
int vendorId = id ?? model.Id;
|
||||
if (vendorId == 0)
|
||||
return BadRequest(new Response { Status = "Error", Message = "Vendor Id is required" });
|
||||
|
||||
var updateDto = new UpdateVendorDTO
|
||||
{
|
||||
Name = model.CompanyName
|
||||
};
|
||||
|
||||
var userId = User.Identity?.Name ?? "system";
|
||||
await _vendorService.UpdateVendorAsync(id, updateDto, userId);
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (userId == null)
|
||||
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
||||
|
||||
await _vendorService.UpdateVendorAsync(vendorId, updateDto, userId);
|
||||
return Ok(new DataResponse { Message = "Vendor Updated", Status = "200" });
|
||||
}
|
||||
catch (ValidationException vex)
|
||||
{
|
||||
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
|
||||
return BadRequest(new Response { Status = "Validation Error", Message = errors });
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
return NotFound(new Response { Status = "Error", Message = "Vendor not found" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||||
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> Delete(int id)
|
||||
[HttpPost("Delete")]
|
||||
public async Task<IActionResult> Delete([FromRoute] int? id, [FromQuery] int? queryId = null)
|
||||
{
|
||||
var userId = User.Identity?.Name ?? "system";
|
||||
await _vendorService.DeleteVendorAsync(id, userId);
|
||||
return Ok(new DataResponse { Message = "Vendor Deleted", Status = "200" });
|
||||
try
|
||||
{
|
||||
// Support both route parameter and query string
|
||||
int vendorId = id ?? queryId ?? 0;
|
||||
if (vendorId == 0)
|
||||
return BadRequest(new Response { Status = "Error", Message = "Vendor Id is required" });
|
||||
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (userId == null)
|
||||
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
||||
|
||||
await _vendorService.DeleteVendorAsync(vendorId, userId);
|
||||
return Ok(new DataResponse { Message = "Vendor Deleted", Status = "200" });
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
return NotFound(new Response { Status = "Error", Message = "Vendor not found" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("Dropdown")]
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,5 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
|
||||
namespace Api.SeaHavenIndustries.DTOs
|
||||
{
|
||||
|
|
@ -9,14 +10,11 @@ namespace Api.SeaHavenIndustries.DTOs
|
|||
public string? Description { get; set; }
|
||||
public string? PO { get; set; }
|
||||
public string? TT { get; set; }
|
||||
//public int? ContactId { get; set; }
|
||||
public string? AssignTo { get; set; }
|
||||
public DateTime? DueDate { get; set; }
|
||||
public string? Priority { get; set; }
|
||||
public WorkOrderPriority? Priority { get; set; }
|
||||
public int? LocationId { get; set; }
|
||||
public bool? istemplate { get; set; }
|
||||
//public int? CategoryId { get; set; }
|
||||
|
||||
public List<IFormFile>? Attachments { get; set; }
|
||||
public string? BeforPhotoIssue { get; set; }
|
||||
public IFormFile? BeforPhotoAttachment { get; set; }
|
||||
|
|
@ -25,15 +23,33 @@ namespace Api.SeaHavenIndustries.DTOs
|
|||
public string? SignOffName { get; set; }
|
||||
public IFormFile? SignOffAttachment { get; set; }
|
||||
public string? SignOffSignature { get; set; }
|
||||
public string? WorkOrderContacts { get; set; }
|
||||
public string? WorkOrderCategories { get; set; }
|
||||
public List<int> GetContactIds()
|
||||
public List<int>? ContactIds { get; set; }
|
||||
public List<int>? CategoryIds { get; set; }
|
||||
|
||||
public static WorkOrder MapToEntity(Workorder_DTO dto, string userId)
|
||||
{
|
||||
return WorkOrderContacts?.Split(',').Select(int.Parse).ToList() ?? new List<int>();
|
||||
}
|
||||
public List<int> GetCategoryIds()
|
||||
{
|
||||
return WorkOrderCategories?.Split(',').Select(int.Parse).ToList() ?? new List<int>();
|
||||
if (dto == null) return null!;
|
||||
|
||||
return new WorkOrder
|
||||
{
|
||||
WorkerOrderNumber = dto.WorkerOrderNumber,
|
||||
WorkerOrderTitle = dto.WorkerOrderTitle,
|
||||
Description = dto.Description,
|
||||
PO = dto.PO,
|
||||
TT = dto.TT,
|
||||
AssignTo = dto.AssignTo,
|
||||
DueDate = dto.DueDate,
|
||||
Priority = (dto.Priority ?? WorkOrderPriority.Medium).ToString(),
|
||||
LocationId = dto.LocationId,
|
||||
istemplate = dto.istemplate ?? false,
|
||||
Status = WorkOrderStatus.Open.ToString(),
|
||||
BeforPhotoIssue = dto.BeforPhotoIssue,
|
||||
AfterPhotoIssue = dto.AfterPhotoIssue,
|
||||
SignOffName = dto.SignOffName,
|
||||
SignOffSignature = dto.SignOffSignature,
|
||||
createdby = userId,
|
||||
CreatedDate = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
}
|
||||
public class EditWorkorder_DTO
|
||||
|
|
@ -46,8 +62,8 @@ namespace Api.SeaHavenIndustries.DTOs
|
|||
public string? TT { get; set; }
|
||||
public string? AssignTo { get; set; }
|
||||
public DateTime? DueDate { get; set; }
|
||||
public string? Priority { get; set; }
|
||||
public string? Status { get; set; }
|
||||
public WorkOrderPriority? Priority { get; set; }
|
||||
public WorkOrderStatus? Status { get; set; }
|
||||
public int? LocationId { get; set; }
|
||||
public string? Problem { get; set; }
|
||||
public string? Trade { get; set; }
|
||||
|
|
@ -56,7 +72,6 @@ namespace Api.SeaHavenIndustries.DTOs
|
|||
public DateTime? ScheduledDate { get; set; }
|
||||
public DateTime? CompletedDate { get; set; }
|
||||
public string? Source { get; set; }
|
||||
|
||||
public List<IFormFile>? Attachments { get; set; }
|
||||
public string? BeforPhotoIssue { get; set; }
|
||||
public IFormFile? BeforPhotoAttachment { get; set; }
|
||||
|
|
@ -65,16 +80,36 @@ namespace Api.SeaHavenIndustries.DTOs
|
|||
public string? SignOffName { get; set; }
|
||||
public IFormFile? SignOffAttachment { get; set; }
|
||||
public string? SignOffSignature { get; set; }
|
||||
public string? WorkOrderContacts { get; set; }
|
||||
public string? WorkOrderCategories { get; set; }
|
||||
public List<int> GetContactIds()
|
||||
public List<int>? ContactIds { get; set; }
|
||||
public List<int>? CategoryIds { get; set; }
|
||||
|
||||
public static void UpdateEntity(EditWorkorder_DTO dto, WorkOrder entity)
|
||||
{
|
||||
return WorkOrderContacts?.Split(',').Select(int.Parse).ToList() ?? new List<int>();
|
||||
}
|
||||
public List<int> GetCategoryIds()
|
||||
{
|
||||
return WorkOrderCategories?.Split(',').Select(int.Parse).ToList() ?? new List<int>();
|
||||
if (dto == null || entity == null) return;
|
||||
|
||||
if (dto.WorkerOrderNumber != null) entity.WorkerOrderNumber = dto.WorkerOrderNumber;
|
||||
if (dto.WorkerOrderTitle != null) entity.WorkerOrderTitle = dto.WorkerOrderTitle;
|
||||
if (dto.Description != null) entity.Description = dto.Description;
|
||||
if (dto.PO != null) entity.PO = dto.PO;
|
||||
if (dto.TT != null) entity.TT = dto.TT;
|
||||
if (dto.AssignTo != null) entity.AssignTo = dto.AssignTo;
|
||||
if (dto.DueDate.HasValue) entity.DueDate = dto.DueDate;
|
||||
if (dto.Priority.HasValue) entity.Priority = dto.Priority.ToString()!;
|
||||
if (dto.Status.HasValue) entity.Status = dto.Status.ToString()!;
|
||||
if (dto.LocationId.HasValue) entity.LocationId = dto.LocationId;
|
||||
if (dto.Problem != null) entity.Problem = dto.Problem;
|
||||
if (dto.Trade != null) entity.Trade = dto.Trade;
|
||||
if (dto.SubTrade != null) entity.SubTrade = dto.SubTrade;
|
||||
if (dto.VendorNTE.HasValue) entity.VendorNTE = dto.VendorNTE;
|
||||
if (dto.ScheduledDate.HasValue) entity.ScheduledDate = dto.ScheduledDate;
|
||||
if (dto.CompletedDate.HasValue) entity.CompletedDate = dto.CompletedDate;
|
||||
if (dto.Source != null) entity.Source = dto.Source;
|
||||
if (dto.BeforPhotoIssue != null) entity.BeforPhotoIssue = dto.BeforPhotoIssue;
|
||||
if (dto.AfterPhotoIssue != null) entity.AfterPhotoIssue = dto.AfterPhotoIssue;
|
||||
if (dto.SignOffName != null) entity.SignOffName = dto.SignOffName;
|
||||
if (dto.SignOffSignature != null) entity.SignOffSignature = dto.SignOffSignature;
|
||||
|
||||
entity.LastModificationTime = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@ using Microsoft.EntityFrameworkCore;
|
|||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using System.Text;
|
||||
using SeaHaven.DataServices.DependencyInjection;
|
||||
using SeaHaven.Services.DependencyInjection;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
ConfigurationManager configuration = builder.Configuration;
|
||||
|
|
@ -31,7 +33,7 @@ builder.Services.AddResponseCompression(opts =>
|
|||
});
|
||||
builder.Services.AddCors(option =>
|
||||
option.AddDefaultPolicy(builder => builder.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod()));
|
||||
// Helper services
|
||||
|
||||
builder.Services.AddScoped<SendMessage>();
|
||||
builder.Services.AddScoped<Api.SeaHavenIndustries.Helper.VendorPortalTokenService>();
|
||||
builder.Services.AddSingleton<Api.SeaHavenIndustries.Helper.ZipCodeDistance>(sp =>
|
||||
|
|
@ -43,36 +45,8 @@ builder.Services.AddSingleton<Api.SeaHavenIndustries.Helper.ZipCodeDistance>(sp
|
|||
return zipService;
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// MULTI-TIER ARCHITECTURE - Dependency Injection Registration
|
||||
// ========================================================================
|
||||
|
||||
// Data Services (Data Access Layer)
|
||||
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.IContactDataService, SeaHaven.DataServices.Implementation.ContactDataService>();
|
||||
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.IWorkOrderDataService, SeaHaven.DataServices.Implementation.WorkOrderDataService>();
|
||||
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.ILocationDataService, SeaHaven.DataServices.Implementation.LocationDataService>();
|
||||
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.IEmployeeDataService, SeaHaven.DataServices.Implementation.EmployeeDataService>();
|
||||
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.IAccountDataService, SeaHaven.DataServices.Implementation.AccountDataService>();
|
||||
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.IVendorDataService, SeaHaven.DataServices.Implementation.VendorDataService>();
|
||||
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.IDispatchDataService, SeaHaven.DataServices.Implementation.DispatchDataService>();
|
||||
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.IFollowUpDataService, SeaHaven.DataServices.Implementation.FollowUpDataService>();
|
||||
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.IPMScheduleDataService, SeaHaven.DataServices.Implementation.PMScheduleDataService>();
|
||||
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.IAssetDataService, SeaHaven.DataServices.Implementation.AssetDataService>();
|
||||
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.ICategoryDataService, SeaHaven.DataServices.Implementation.CategoryDataService>();
|
||||
|
||||
// Business Services (Business Logic Layer)
|
||||
builder.Services.AddScoped<SeaHaven.Services.Interfaces.IContactService, SeaHaven.Services.Implementation.ContactService>();
|
||||
builder.Services.AddScoped<SeaHaven.Services.Interfaces.IWorkOrderService, SeaHaven.Services.Implementation.WorkOrderService>();
|
||||
builder.Services.AddScoped<SeaHaven.Services.Interfaces.ILocationService, SeaHaven.Services.Implementation.LocationService>();
|
||||
builder.Services.AddScoped<SeaHaven.Services.Interfaces.IEmployeeService, SeaHaven.Services.Implementation.EmployeeService>();
|
||||
builder.Services.AddScoped<SeaHaven.Services.Interfaces.IAccountService, SeaHaven.Services.Implementation.AccountService>();
|
||||
builder.Services.AddScoped<SeaHaven.Services.Interfaces.IVendorService, SeaHaven.Services.Implementation.VendorService>();
|
||||
builder.Services.AddScoped<SeaHaven.Services.Interfaces.IDispatchService, SeaHaven.Services.Implementation.DispatchService>();
|
||||
builder.Services.AddScoped<SeaHaven.Services.Interfaces.IFollowUpService, SeaHaven.Services.Implementation.FollowUpService>();
|
||||
builder.Services.AddScoped<SeaHaven.Services.Interfaces.IPMScheduleService, SeaHaven.Services.Implementation.PMScheduleService>();
|
||||
builder.Services.AddScoped<SeaHaven.Services.Interfaces.IAssetService, SeaHaven.Services.Implementation.AssetService>();
|
||||
|
||||
// ========================================================================
|
||||
builder.Services.AddDataServices();
|
||||
builder.Services.AddBusinessServices();
|
||||
|
||||
builder.Services.AddAuthentication(options =>
|
||||
{
|
||||
|
|
|
|||
531
BACKEND_ARCHITECTURE.md
Normal file
531
BACKEND_ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,531 @@
|
|||
# Backend Architecture Guide
|
||||
|
||||
## 🏗️ Clean Architecture Overview
|
||||
|
||||
The backend follows **Clean Architecture** with strict layer separation:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ API LAYER (Controllers) │
|
||||
│ - HTTP Request/Response │
|
||||
│ - Authentication & Authorization │
|
||||
│ - Exception Handling │
|
||||
│ - Input Validation (basic) │
|
||||
└─────────────────┬───────────────────────────────────────────┘
|
||||
│ Calls
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ SERVICE LAYER (Business Logic) │
|
||||
│ - Business Rules │
|
||||
│ - FluentValidation (detailed validation) │
|
||||
│ - Data Transformation │
|
||||
│ - Orchestration │
|
||||
└─────────────────┬───────────────────────────────────────────┘
|
||||
│ Calls
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ DATA SERVICE LAYER (Persistence) │
|
||||
│ - Database Access │
|
||||
│ - Entity Operations (CRUD) │
|
||||
│ - Query Building │
|
||||
│ - Transaction Management │
|
||||
└─────────────────┬───────────────────────────────────────────┘
|
||||
│ Uses
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ DATA LAYER (EF Core) │
|
||||
│ - DbContext │
|
||||
│ - Entity Models │
|
||||
│ - Database Schema │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
backend/shoc-backend/
|
||||
│
|
||||
├── Api.SeaHavenIndustries/ # 🌐 API Layer
|
||||
│ ├── Controllers/ # HTTP endpoints
|
||||
│ ├── DTOs/ # Data Transfer Objects (API contracts)
|
||||
│ ├── Program.cs # App startup & DI registration
|
||||
│ └── appsettings.json # Configuration
|
||||
│
|
||||
├── SeaHaven.Services/ # 💼 Service Layer (Business Logic)
|
||||
│ ├── Interfaces/ # Service contracts (IContactService, etc.)
|
||||
│ ├── Implementation/ # Service implementations
|
||||
│ ├── DTOs/ # Internal DTOs for service layer
|
||||
│ └── Validators/ # FluentValidation validators
|
||||
│
|
||||
├── SeaHaven.DataServices/ # 💾 Data Access Layer
|
||||
│ ├── Interfaces/ # DataService contracts (IContactDataService, etc.)
|
||||
│ ├── Implementation/ # DataService implementations
|
||||
│ └── DTOs/ # Data layer DTOs
|
||||
│
|
||||
└── Data.SeaHavenIndustries/ # 🗄️ Data Layer (EF Core)
|
||||
├── Models/ # Entity models (Contact, Employee, etc.)
|
||||
├── ApplicationDbContext.cs # EF Core DbContext
|
||||
└── Migrations/ # Database migrations
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Data Flow Example (Creating a Contact)
|
||||
|
||||
### 1️⃣ **API Layer** (`ContactController.cs`)
|
||||
```csharp
|
||||
[HttpPost("Create")]
|
||||
public async Task<IActionResult> AddContacts(Contact_DTO dto)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get authenticated user ID
|
||||
var userId = _userManager.GetUserId(User);
|
||||
if (userId == null)
|
||||
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
||||
|
||||
// Convert API DTO → Service DTO
|
||||
var serviceDto = dto.ToServiceCreateDTO();
|
||||
|
||||
// Call Service Layer
|
||||
await _contactService.CreateContactAsync(serviceDto, userId);
|
||||
|
||||
return Ok(new Response { Status = "Success", Message = "Contact created" });
|
||||
}
|
||||
catch (ValidationException vex)
|
||||
{
|
||||
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
|
||||
return BadRequest(new Response { Status = "Validation Error", Message = errors });
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
return NotFound(new Response { Status = "Error", Message = "Resource not found" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Responsibilities:**
|
||||
- ✅ Authenticate user
|
||||
- ✅ Extract user ID from claims
|
||||
- ✅ Map API DTO to Service DTO
|
||||
- ✅ Catch exceptions and return proper HTTP status codes
|
||||
- ❌ NO business logic
|
||||
- ❌ NO database access
|
||||
|
||||
---
|
||||
|
||||
### 2️⃣ **Service Layer** (`ContactService.cs`)
|
||||
|
||||
```csharp
|
||||
public class ContactService : IContactService
|
||||
{
|
||||
private readonly IContactDataService _dataService;
|
||||
private readonly IValidator<CreateContactDTO> _createValidator;
|
||||
|
||||
public ContactService(
|
||||
IContactDataService dataService,
|
||||
IValidator<CreateContactDTO> createValidator)
|
||||
{
|
||||
_dataService = dataService;
|
||||
_createValidator = createValidator;
|
||||
}
|
||||
|
||||
public async Task<ContactDTO> CreateContactAsync(CreateContactDTO dto, string userId)
|
||||
{
|
||||
// STEP 1: Validate using FluentValidation
|
||||
var validationResult = await _createValidator.ValidateAsync(dto);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ValidationException(validationResult.Errors);
|
||||
}
|
||||
|
||||
// STEP 2: Business logic (if any)
|
||||
// Example: Check for duplicates, apply business rules, etc.
|
||||
|
||||
// STEP 3: Map Service DTO → Data DTO
|
||||
var dataDto = new CreateContactDataDTO
|
||||
{
|
||||
FirstName = dto.FirstName,
|
||||
LastName = dto.LastName,
|
||||
Email = dto.Email,
|
||||
Phone = dto.Phone
|
||||
};
|
||||
|
||||
// STEP 4: Call Data Service Layer
|
||||
var createdContact = await _dataService.CreateAsync(dataDto, userId);
|
||||
|
||||
// STEP 5: Map Data DTO → Service DTO and return
|
||||
return new ContactDTO
|
||||
{
|
||||
Id = createdContact.Id,
|
||||
FirstName = createdContact.FirstName,
|
||||
LastName = createdContact.LastName,
|
||||
Email = createdContact.Email,
|
||||
Phone = createdContact.Phone
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Responsibilities:**
|
||||
- ✅ Validate input using FluentValidation
|
||||
- ✅ Apply business rules
|
||||
- ✅ Orchestrate operations
|
||||
- ✅ Transform data between layers
|
||||
- ❌ NO HTTP concerns
|
||||
- ❌ NO direct database access
|
||||
|
||||
---
|
||||
|
||||
### 3️⃣ **Data Service Layer** (`ContactDataService.cs`)
|
||||
|
||||
```csharp
|
||||
public class ContactDataService : IContactDataService
|
||||
{
|
||||
private readonly ApplicationDbContext _db;
|
||||
|
||||
public ContactDataService(ApplicationDbContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task<ContactDataDTO> CreateAsync(CreateContactDataDTO dto, string userId)
|
||||
{
|
||||
// STEP 1: Create entity from DTO
|
||||
var contact = new Contact
|
||||
{
|
||||
FirstName = dto.FirstName,
|
||||
LastName = dto.LastName,
|
||||
Email = dto.Email,
|
||||
Phone = dto.Phone,
|
||||
CreatedBy = userId,
|
||||
CreatedDate = DateTime.UtcNow,
|
||||
ModifiedBy = userId,
|
||||
ModifiedDate = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// STEP 2: Add to database
|
||||
_db.Contacts.Add(contact);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
// STEP 3: Map entity → Data DTO and return
|
||||
return new ContactDataDTO
|
||||
{
|
||||
Id = contact.Id,
|
||||
FirstName = contact.FirstName,
|
||||
LastName = contact.LastName,
|
||||
Email = contact.Email,
|
||||
Phone = contact.Phone
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Responsibilities:**
|
||||
- ✅ Direct database operations (CRUD)
|
||||
- ✅ Entity mapping
|
||||
- ✅ Set audit fields (CreatedBy, CreatedDate, etc.)
|
||||
- ✅ Transaction management
|
||||
- ❌ NO validation
|
||||
- ❌ NO business logic
|
||||
|
||||
---
|
||||
|
||||
## 🔌 Dependency Injection (DI)
|
||||
|
||||
### Automatic Assembly Scanning
|
||||
|
||||
Services are **automatically registered** by scanning assemblies using naming convention.
|
||||
|
||||
**In `Program.cs`:**
|
||||
```csharp
|
||||
builder.Services.AddDataServices(); // Auto-register all Data Services
|
||||
builder.Services.AddBusinessServices(); // Auto-register all Business Services + Validators
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
**Module Files:**
|
||||
- `SeaHaven.DataServices/DependencyInjection/DataServicesModule.cs`
|
||||
- `SeaHaven.Services/DependencyInjection/ServicesModule.cs`
|
||||
|
||||
**Process:**
|
||||
1. Scans assembly for all classes
|
||||
2. Finds interface matching pattern `I{ClassName}`
|
||||
3. Registers as Scoped service
|
||||
4. Also auto-registers FluentValidation validators
|
||||
|
||||
### Naming Convention Required
|
||||
|
||||
| Interface | Implementation | Registered? |
|
||||
|-----------|---------------|-------------|
|
||||
| `IContactService` | `ContactService` | ✅ YES |
|
||||
| `IContactDataService` | `ContactDataService` | ✅ YES |
|
||||
| `ISomething` | `SomethingDifferent` | ❌ NO |
|
||||
|
||||
**Rule**: Interface must be `I` + Class name exactly
|
||||
|
||||
### Usage in Code
|
||||
|
||||
Once registered, inject via constructor:
|
||||
|
||||
```csharp
|
||||
public class ContactController : Controller
|
||||
{
|
||||
private readonly IContactService _contactService;
|
||||
|
||||
public ContactController(IContactService contactService)
|
||||
{
|
||||
_contactService = contactService;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Benefits
|
||||
|
||||
✅ No manual registration - just follow naming convention
|
||||
✅ Auto-discovery - new services work automatically
|
||||
✅ FluentValidation auto-registered
|
||||
|
||||
---
|
||||
|
||||
## ✅ FluentValidation
|
||||
|
||||
### Validator Example (`CreateContactDTOValidator.cs`)
|
||||
|
||||
```csharp
|
||||
public class CreateContactDTOValidator : AbstractValidator<CreateContactDTO>
|
||||
{
|
||||
public CreateContactDTOValidator()
|
||||
{
|
||||
RuleFor(x => x.FirstName)
|
||||
.NotEmpty().WithMessage("First name is required")
|
||||
.MaximumLength(50).WithMessage("First name cannot exceed 50 characters");
|
||||
|
||||
RuleFor(x => x.LastName)
|
||||
.NotEmpty().WithMessage("Last name is required")
|
||||
.MaximumLength(50).WithMessage("Last name cannot exceed 50 characters");
|
||||
|
||||
RuleFor(x => x.Email)
|
||||
.NotEmpty().WithMessage("Email is required")
|
||||
.EmailAddress().WithMessage("Invalid email format")
|
||||
.MaximumLength(100).WithMessage("Email cannot exceed 100 characters");
|
||||
|
||||
RuleFor(x => x.Phone)
|
||||
.Matches(@"^\+?[1-9]\d{1,14}$").When(x => !string.IsNullOrEmpty(x.Phone))
|
||||
.WithMessage("Invalid phone number format");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Where Validation Happens
|
||||
|
||||
```
|
||||
❌ Controller → NO detailed validation (only catches ValidationException)
|
||||
✅ Service → YES - FluentValidation runs here
|
||||
❌ DataService → NO validation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Interface Examples
|
||||
|
||||
### Service Interface (`IContactService.cs`)
|
||||
```csharp
|
||||
public interface IContactService
|
||||
{
|
||||
Task<ContactDTO> CreateContactAsync(CreateContactDTO dto, string userId);
|
||||
Task<ContactDTO> UpdateContactAsync(int id, UpdateContactDTO dto, string userId);
|
||||
Task DeleteContactAsync(int id, string userId);
|
||||
Task<ContactDTO> GetContactByIdAsync(int id);
|
||||
Task<PagedResult<ContactDTO>> GetContactsPagedAsync(int page, int pageSize, string? search);
|
||||
}
|
||||
```
|
||||
|
||||
### Data Service Interface (`IContactDataService.cs`)
|
||||
```csharp
|
||||
public interface IContactDataService
|
||||
{
|
||||
Task<ContactDataDTO> CreateAsync(CreateContactDataDTO dto, string userId);
|
||||
Task<ContactDataDTO> UpdateAsync(int id, UpdateContactDataDTO dto, string userId);
|
||||
Task DeleteAsync(int id, string userId);
|
||||
Task<ContactDataDTO?> GetByIdAsync(int id);
|
||||
Task<PagedResult<ContactDataDTO>> GetPagedAsync(int page, int pageSize, string? search);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 DTO Types
|
||||
|
||||
### 1. **API DTOs** (`Api.SeaHavenIndustries/DTOs/`)
|
||||
- Used for HTTP requests/responses
|
||||
- Map to frontend JSON structure
|
||||
- Example: `Contact_DTO`, `EditContact_DTO`
|
||||
|
||||
### 2. **Service DTOs** (`SeaHaven.Services/DTOs/`)
|
||||
- Used between Controller ↔ Service
|
||||
- Business-focused structure
|
||||
- Example: `CreateContactDTO`, `UpdateContactDTO`, `ContactDTO`
|
||||
|
||||
### 3. **Data DTOs** (`SeaHaven.DataServices/DTOs/`)
|
||||
- Used between Service ↔ DataService
|
||||
- Database-focused structure
|
||||
- Example: `CreateContactDataDTO`, `ContactDataDTO`
|
||||
|
||||
### DTO Mapping Flow
|
||||
```
|
||||
API DTO → Service DTO → Data DTO → Entity
|
||||
(HTTP) (Business) (Data) (Database)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚨 Exception Handling Pattern
|
||||
|
||||
### Controller Exception Handling (STANDARD PATTERN)
|
||||
|
||||
```csharp
|
||||
[HttpPost("Create")]
|
||||
public async Task<IActionResult> Create([FromBody] SomeDTO dto)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get user ID
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (userId == null)
|
||||
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
||||
|
||||
// Call service
|
||||
await _service.CreateAsync(dto, userId);
|
||||
|
||||
return Ok(new Response { Status = "Success", Message = "Created successfully" });
|
||||
}
|
||||
catch (ValidationException vex) // ⚠️ CATCH THIS FIRST
|
||||
{
|
||||
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
|
||||
return BadRequest(new Response { Status = "Validation Error", Message = errors });
|
||||
}
|
||||
catch (KeyNotFoundException) // ⚠️ THEN THIS
|
||||
{
|
||||
return NotFound(new Response { Status = "Error", Message = "Not found" });
|
||||
}
|
||||
catch (Exception ex) // ⚠️ GENERIC LAST
|
||||
{
|
||||
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### HTTP Status Code Mapping
|
||||
- **200 OK** → Success
|
||||
- **400 Bad Request** → Validation errors
|
||||
- **401 Unauthorized** → Not authenticated
|
||||
- **404 Not Found** → Resource not found
|
||||
- **500 Internal Server Error** → Unexpected errors
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Authentication & User ID
|
||||
|
||||
### Getting User ID (Two Methods)
|
||||
|
||||
**Method 1: Using Claims (Preferred)**
|
||||
```csharp
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (userId == null)
|
||||
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
||||
```
|
||||
|
||||
**Method 2: Using UserManager (for Identity)**
|
||||
```csharp
|
||||
var userId = _userManager.GetUserId(User);
|
||||
if (userId == null)
|
||||
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
||||
```
|
||||
|
||||
### Audit Fields
|
||||
Every entity should track:
|
||||
- `CreatedBy` (string - User ID GUID)
|
||||
- `CreatedDate` (DateTime)
|
||||
- `ModifiedBy` (string - User ID GUID)
|
||||
- `ModifiedDate` (DateTime)
|
||||
|
||||
---
|
||||
|
||||
## 📋 Naming Conventions
|
||||
|
||||
### Controllers
|
||||
- Named after entity + "Controller"
|
||||
- Example: `ContactController`, `EmployeeController`
|
||||
|
||||
### Services
|
||||
- Interface: `I[Entity]Service`
|
||||
- Implementation: `[Entity]Service`
|
||||
- Example: `IContactService`, `ContactService`
|
||||
|
||||
### Data Services
|
||||
- Interface: `I[Entity]DataService`
|
||||
- Implementation: `[Entity]DataService`
|
||||
- Example: `IContactDataService`, `ContactDataService`
|
||||
|
||||
### DTOs
|
||||
- API Layer: `[Entity]_DTO`, `Edit[Entity]_DTO`
|
||||
- Service Layer: `Create[Entity]DTO`, `Update[Entity]DTO`, `[Entity]DTO`
|
||||
- Data Layer: `Create[Entity]DataDTO`, `[Entity]DataDTO`
|
||||
|
||||
---
|
||||
|
||||
## ✅ Best Practices Checklist
|
||||
|
||||
### Controllers
|
||||
- [ ] Use dependency injection for services
|
||||
- [ ] Get user ID from `User.FindFirstValue(ClaimTypes.NameIdentifier)`
|
||||
- [ ] Return 401 if userId is null
|
||||
- [ ] Catch `ValidationException` BEFORE generic `Exception`
|
||||
- [ ] Return appropriate HTTP status codes
|
||||
- [ ] NO business logic in controllers
|
||||
- [ ] NO direct database access (`_db`)
|
||||
|
||||
### Services
|
||||
- [ ] Validate using FluentValidation
|
||||
- [ ] Throw `ValidationException` for validation errors
|
||||
- [ ] Throw `KeyNotFoundException` for not found
|
||||
- [ ] Pass userId to DataService
|
||||
- [ ] Transform DTOs between layers
|
||||
- [ ] Implement business rules here
|
||||
|
||||
### Data Services
|
||||
- [ ] Direct database access only
|
||||
- [ ] Set audit fields (CreatedBy, CreatedDate, ModifiedBy, ModifiedDate)
|
||||
- [ ] Use async/await for all database operations
|
||||
- [ ] NO validation logic
|
||||
- [ ] NO business logic
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Quick Reference
|
||||
|
||||
| Layer | Purpose | Can Access | Cannot Access |
|
||||
|-------|---------|-----------|---------------|
|
||||
| **Controller** | HTTP handling, auth | Service layer | Database directly |
|
||||
| **Service** | Business logic, validation | DataService layer | Database directly |
|
||||
| **DataService** | Database operations | DbContext, Entities | HTTP context |
|
||||
| **Data** | Entity definitions | Nothing (POCOs) | Application logic |
|
||||
|
||||
---
|
||||
|
||||
## 📚 Example: Full CRUD Implementation
|
||||
|
||||
See these files for complete examples:
|
||||
- **Contact**: `ContactController.cs`, `ContactService.cs`, `ContactDataService.cs`
|
||||
- **Employee**: `EmployeeController.cs`, `EmployeeService.cs`, `EmployeeDataService.cs`
|
||||
- **Asset**: `AssetController.cs`, `AssetService.cs`, `AssetDataService.cs`
|
||||
|
||||
All follow the same Clean Architecture pattern!
|
||||
237
DI_ASSEMBLY_SCANNING_GUIDE.md
Normal file
237
DI_ASSEMBLY_SCANNING_GUIDE.md
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
# Dependency Injection - Assembly Scanning Guide
|
||||
|
||||
## 🎯 Overview
|
||||
|
||||
This project uses **assembly scanning** to automatically register services instead of manual registration. This means you don't need to add each service to `Program.cs` individually.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 How It Works
|
||||
|
||||
### In `Program.cs`
|
||||
|
||||
```csharp
|
||||
// These two lines register ALL services automatically
|
||||
builder.Services.AddDataServices(); // Scans SeaHaven.DataServices assembly
|
||||
builder.Services.AddBusinessServices(); // Scans SeaHaven.Services assembly + Validators
|
||||
```
|
||||
|
||||
That's it! No need for this:
|
||||
```csharp
|
||||
// ❌ DON'T DO THIS - It's automatic now!
|
||||
builder.Services.AddScoped<IContactDataService, ContactDataService>();
|
||||
builder.Services.AddScoped<IEmployeeDataService, EmployeeDataService>();
|
||||
builder.Services.AddScoped<IAssetDataService, AssetDataService>();
|
||||
// ... hundreds of lines
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 DataServicesModule
|
||||
|
||||
**Location**: `SeaHaven.DataServices/DependencyInjection/DataServicesModule.cs`
|
||||
|
||||
```csharp
|
||||
public static IServiceCollection AddDataServices(this IServiceCollection services)
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
|
||||
// Find all non-abstract classes in the assembly
|
||||
var allClasses = assembly.GetTypes()
|
||||
.Where(t => t.IsClass && !t.IsAbstract && !t.IsGenericType)
|
||||
.ToList();
|
||||
|
||||
foreach (var implementationType in allClasses)
|
||||
{
|
||||
// Find interface matching pattern: I{ClassName}
|
||||
var defaultInterface = implementationType.GetInterfaces()
|
||||
.FirstOrDefault(i => i.Name == $"I{implementationType.Name}");
|
||||
|
||||
if (defaultInterface != null)
|
||||
{
|
||||
// Register as Scoped
|
||||
services.AddScoped(defaultInterface, implementationType);
|
||||
}
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
```
|
||||
|
||||
### What It Does
|
||||
|
||||
1. Scans entire `SeaHaven.DataServices` assembly
|
||||
2. Finds all concrete classes (e.g., `ContactDataService`, `EmployeeDataService`)
|
||||
3. For each class, looks for an interface named `I{ClassName}`
|
||||
4. If found, registers the pair as **Scoped** service
|
||||
|
||||
### Example
|
||||
|
||||
If you create:
|
||||
- `IProductDataService` (interface)
|
||||
- `ProductDataService` (class implementing `IProductDataService`)
|
||||
|
||||
Then `IProductDataService → ProductDataService` is **automatically registered** as Scoped.
|
||||
|
||||
---
|
||||
|
||||
## 💼 ServicesModule
|
||||
|
||||
**Location**: `SeaHaven.Services/DependencyInjection/ServicesModule.cs`
|
||||
|
||||
```csharp
|
||||
public static IServiceCollection AddBusinessServices(this IServiceCollection services)
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
|
||||
var allClasses = assembly.GetTypes()
|
||||
.Where(t => t.IsClass && !t.IsAbstract && !t.IsGenericType)
|
||||
.ToList();
|
||||
|
||||
foreach (var implementationType in allClasses)
|
||||
{
|
||||
var defaultInterface = implementationType.GetInterfaces()
|
||||
.FirstOrDefault(i => i.Name == $"I{implementationType.Name}");
|
||||
|
||||
if (defaultInterface != null)
|
||||
{
|
||||
services.AddScoped(defaultInterface, implementationType);
|
||||
}
|
||||
}
|
||||
|
||||
// ALSO auto-register all FluentValidation validators
|
||||
services.AddValidatorsFromAssembly(assembly);
|
||||
|
||||
return services;
|
||||
}
|
||||
```
|
||||
|
||||
### What It Does
|
||||
|
||||
1. Scans entire `SeaHaven.Services` assembly
|
||||
2. Registers all services using naming convention (same as DataServices)
|
||||
3. **BONUS**: Automatically registers ALL FluentValidation validators in the assembly
|
||||
|
||||
### Example
|
||||
|
||||
If you create:
|
||||
- `IProductService` (interface)
|
||||
- `ProductService` (class)
|
||||
- `CreateProductDTOValidator` (FluentValidation validator)
|
||||
|
||||
Then **all three** are automatically registered.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ CRITICAL: Naming Convention
|
||||
|
||||
For auto-registration to work, you MUST follow this pattern:
|
||||
|
||||
### ✅ CORRECT
|
||||
|
||||
| Interface | Implementation | Result |
|
||||
|-----------|---------------|--------|
|
||||
| `IContactService` | `ContactService` | ✅ Registered as Scoped |
|
||||
| `IContactDataService` | `ContactDataService` | ✅ Registered as Scoped |
|
||||
| `IProductService` | `ProductService` | ✅ Registered as Scoped |
|
||||
| `IEmployeeService` | `EmployeeService` | ✅ Registered as Scoped |
|
||||
|
||||
### ❌ WRONG
|
||||
|
||||
| Interface | Implementation | Result |
|
||||
|-----------|---------------|--------|
|
||||
| `IProductService` | `ProductServiceImpl` | ❌ NOT registered - names don't match |
|
||||
| `IContactRepository` | `ContactService` | ❌ NOT registered - names don't match |
|
||||
| `ISomething` | `SomethingElse` | ❌ NOT registered - names don't match |
|
||||
|
||||
---
|
||||
|
||||
## 📋 Rules
|
||||
|
||||
1. **Interface naming**: `I{ClassName}`
|
||||
2. **Class naming**: `{ClassName}`
|
||||
3. **Example**:
|
||||
- Interface: `IContactService`
|
||||
- Class: `ContactService` (NOT `ContactServiceImpl`, `ContactServiceImplementation`, etc.)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Benefits
|
||||
|
||||
✅ **Less boilerplate** - No need to add each service to `Program.cs`
|
||||
✅ **Auto-discovery** - New services are automatically registered
|
||||
✅ **Consistent** - All services registered the same way
|
||||
✅ **FluentValidation included** - Validators work automatically
|
||||
✅ **Cleaner Program.cs** - Only 2 lines instead of 50+
|
||||
|
||||
---
|
||||
|
||||
## 🎯 How to Add a New Service
|
||||
|
||||
### Old Way (Manual)
|
||||
```csharp
|
||||
// 1. Create interface and class
|
||||
public interface IProductService { }
|
||||
public class ProductService : IProductService { }
|
||||
|
||||
// 2. Manually add to Program.cs
|
||||
builder.Services.AddScoped<IProductService, ProductService>(); // ❌ DON'T DO THIS
|
||||
```
|
||||
|
||||
### New Way (Automatic)
|
||||
```csharp
|
||||
// 1. Create interface and class following naming convention
|
||||
public interface IProductService { }
|
||||
public class ProductService : IProductService { }
|
||||
|
||||
// 2. Done! It's automatically registered ✅
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Lifecycle
|
||||
|
||||
All services are registered as **Scoped**:
|
||||
- New instance per HTTP request
|
||||
- Shared within the same request
|
||||
- Disposed at end of request
|
||||
|
||||
---
|
||||
|
||||
## 📚 Example Flow
|
||||
|
||||
1. You create `IProductDataService` and `ProductDataService` in `SeaHaven.DataServices`
|
||||
2. You create `IProductService` and `ProductService` in `SeaHaven.Services`
|
||||
3. You create `CreateProductDTOValidator` in `SeaHaven.Services/Validation`
|
||||
4. Application starts → `AddDataServices()` and `AddBusinessServices()` run
|
||||
5. Assembly scanner finds your services and validator
|
||||
6. All registered automatically as Scoped
|
||||
7. You inject `IProductService` in controller → works immediately ✅
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Service not being injected?
|
||||
|
||||
Check:
|
||||
1. ✅ Is the interface named `I{ClassName}`?
|
||||
2. ✅ Is the class named `{ClassName}`?
|
||||
3. ✅ Is the class in the correct assembly?
|
||||
4. ✅ Is the class public and concrete (not abstract)?
|
||||
5. ✅ Does the class implement the interface?
|
||||
|
||||
### Validator not working?
|
||||
|
||||
Check:
|
||||
1. ✅ Is the validator in `SeaHaven.Services` assembly?
|
||||
2. ✅ Does it inherit from `AbstractValidator<T>`?
|
||||
3. ✅ Is it public and concrete?
|
||||
|
||||
---
|
||||
|
||||
## 📖 See Also
|
||||
|
||||
- **Full Architecture Guide**: `BACKEND_ARCHITECTURE.md`
|
||||
- **Quick Start Guide**: `QUICK_START_GUIDE.md`
|
||||
- **At a Glance**: `ARCHITECTURE_AT_A_GLANCE.md`
|
||||
92
Data.SeaHavenIndustries/Enums/WorkOrderEnums.cs
Normal file
92
Data.SeaHavenIndustries/Enums/WorkOrderEnums.cs
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
namespace Data.SeaHavenIndustries.Enums
|
||||
{
|
||||
public enum WorkOrderStatus
|
||||
{
|
||||
Open = 1,
|
||||
InProgress = 2,
|
||||
Completed = 3,
|
||||
Cancelled = 4,
|
||||
OnHold = 5
|
||||
}
|
||||
|
||||
public enum WorkOrderPriority
|
||||
{
|
||||
Low = 1,
|
||||
Medium = 2,
|
||||
High = 3,
|
||||
Critical = 4
|
||||
}
|
||||
|
||||
public enum AuditAction
|
||||
{
|
||||
Create = 1,
|
||||
Edit = 2,
|
||||
Delete = 3,
|
||||
StatusChange = 4,
|
||||
AssignmentChange = 5,
|
||||
Dispatch = 6,
|
||||
DispatchUpdate = 7,
|
||||
DispatchVerify = 8
|
||||
}
|
||||
|
||||
public enum ContactType
|
||||
{
|
||||
Primary = 1,
|
||||
Secondary = 2,
|
||||
Billing = 3,
|
||||
Technical = 4
|
||||
}
|
||||
|
||||
public enum AssetStatus
|
||||
{
|
||||
Active = 1,
|
||||
Inactive = 2,
|
||||
UnderMaintenance = 3,
|
||||
Retired = 4
|
||||
}
|
||||
|
||||
public enum DispatchStatus
|
||||
{
|
||||
Pending = 1,
|
||||
Assigned = 2,
|
||||
InProgress = 3,
|
||||
Completed = 4,
|
||||
Cancelled = 5
|
||||
}
|
||||
|
||||
public enum FollowUpStatus
|
||||
{
|
||||
Open = 1,
|
||||
InProgress = 2,
|
||||
Resolved = 3,
|
||||
Closed = 4
|
||||
}
|
||||
|
||||
public enum PMScheduleFrequency
|
||||
{
|
||||
Daily = 1,
|
||||
Weekly = 2,
|
||||
Monthly = 3,
|
||||
Quarterly = 4,
|
||||
SemiAnnual = 5,
|
||||
Annual = 6
|
||||
}
|
||||
|
||||
public enum CommentType
|
||||
{
|
||||
General = 1,
|
||||
Internal = 2,
|
||||
Customer = 3,
|
||||
Vendor = 4,
|
||||
System = 5
|
||||
}
|
||||
|
||||
public enum RecordType
|
||||
{
|
||||
WorkOrder = 1,
|
||||
Dispatch = 2,
|
||||
Quote = 3,
|
||||
Contact = 4,
|
||||
Asset = 5
|
||||
}
|
||||
}
|
||||
515
QUICK_START_GUIDE.md
Normal file
515
QUICK_START_GUIDE.md
Normal file
|
|
@ -0,0 +1,515 @@
|
|||
# Backend Quick Start Guide
|
||||
|
||||
## 🚀 Adding a New Feature (Step-by-Step)
|
||||
|
||||
### Example: Adding a "Product" Module
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Create Entity (Data Layer)
|
||||
|
||||
**File**: `Data.SeaHavenIndustries/Models/Product.cs`
|
||||
|
||||
```csharp
|
||||
public class Product
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public decimal Price { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
// Audit fields (REQUIRED)
|
||||
public string CreatedBy { get; set; } = string.Empty;
|
||||
public DateTime CreatedDate { get; set; }
|
||||
public string ModifiedBy { get; set; } = string.Empty;
|
||||
public DateTime ModifiedDate { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
**Add to DbContext**: `Data.SeaHavenIndustries/ApplicationDbContext.cs`
|
||||
|
||||
```csharp
|
||||
public DbSet<Product> Products { get; set; }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Create Migration
|
||||
|
||||
```bash
|
||||
cd backend/shoc-backend
|
||||
dotnet ef migrations add AddProductTable --project Data.SeaHavenIndustries --startup-project Api.SeaHavenIndustries
|
||||
dotnet ef database update --project Data.SeaHavenIndustries --startup-project Api.SeaHavenIndustries
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Create DTOs
|
||||
|
||||
### 3A. Data Layer DTOs (`SeaHaven.DataServices/DTOs/`)
|
||||
|
||||
**CreateProductDataDTO.cs**
|
||||
```csharp
|
||||
public class CreateProductDataDTO
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public decimal Price { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
**ProductDataDTO.cs**
|
||||
```csharp
|
||||
public class ProductDataDTO
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public decimal Price { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### 3B. Service Layer DTOs (`SeaHaven.Services/DTOs/`)
|
||||
|
||||
**CreateProductDTO.cs**
|
||||
```csharp
|
||||
public class CreateProductDTO
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public decimal Price { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
**ProductDTO.cs**
|
||||
```csharp
|
||||
public class ProductDTO
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public decimal Price { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### 3C. API Layer DTOs (`Api.SeaHavenIndustries/DTOs/`)
|
||||
|
||||
**Product_DTO.cs**
|
||||
```csharp
|
||||
public class Product_DTO
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public decimal Price { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Create FluentValidation Validator
|
||||
|
||||
**File**: `SeaHaven.Services/Validators/CreateProductDTOValidator.cs`
|
||||
|
||||
```csharp
|
||||
using FluentValidation;
|
||||
using SeaHaven.Services.DTOs;
|
||||
|
||||
public class CreateProductDTOValidator : AbstractValidator<CreateProductDTO>
|
||||
{
|
||||
public CreateProductDTOValidator()
|
||||
{
|
||||
RuleFor(x => x.Name)
|
||||
.NotEmpty().WithMessage("Product name is required")
|
||||
.MaximumLength(100).WithMessage("Name cannot exceed 100 characters");
|
||||
|
||||
RuleFor(x => x.Price)
|
||||
.GreaterThan(0).WithMessage("Price must be greater than 0");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Create DataService
|
||||
|
||||
### 5A. Interface (`SeaHaven.DataServices/Interfaces/IProductDataService.cs`)
|
||||
|
||||
```csharp
|
||||
public interface IProductDataService
|
||||
{
|
||||
Task<ProductDataDTO> CreateAsync(CreateProductDataDTO dto, string userId);
|
||||
Task<ProductDataDTO> UpdateAsync(int id, UpdateProductDataDTO dto, string userId);
|
||||
Task DeleteAsync(int id, string userId);
|
||||
Task<ProductDataDTO?> GetByIdAsync(int id);
|
||||
Task<PagedResult<ProductDataDTO>> GetPagedAsync(int page, int pageSize, string? search);
|
||||
}
|
||||
```
|
||||
|
||||
### 5B. Implementation (`SeaHaven.DataServices/Implementation/ProductDataService.cs`)
|
||||
|
||||
```csharp
|
||||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
public class ProductDataService : IProductDataService
|
||||
{
|
||||
private readonly ApplicationDbContext _db;
|
||||
|
||||
public ProductDataService(ApplicationDbContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task<ProductDataDTO> CreateAsync(CreateProductDataDTO dto, string userId)
|
||||
{
|
||||
var product = new Product
|
||||
{
|
||||
Name = dto.Name,
|
||||
Description = dto.Description,
|
||||
Price = dto.Price,
|
||||
IsActive = true,
|
||||
CreatedBy = userId,
|
||||
CreatedDate = DateTime.UtcNow,
|
||||
ModifiedBy = userId,
|
||||
ModifiedDate = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_db.Products.Add(product);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return new ProductDataDTO
|
||||
{
|
||||
Id = product.Id,
|
||||
Name = product.Name,
|
||||
Description = product.Description,
|
||||
Price = product.Price,
|
||||
IsActive = product.IsActive
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<ProductDataDTO?> GetByIdAsync(int id)
|
||||
{
|
||||
var product = await _db.Products.FindAsync(id);
|
||||
if (product == null) return null;
|
||||
|
||||
return new ProductDataDTO
|
||||
{
|
||||
Id = product.Id,
|
||||
Name = product.Name,
|
||||
Description = product.Description,
|
||||
Price = product.Price,
|
||||
IsActive = product.IsActive
|
||||
};
|
||||
}
|
||||
|
||||
// Implement other methods...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Create Service
|
||||
|
||||
### 6A. Interface (`SeaHaven.Services/Interfaces/IProductService.cs`)
|
||||
|
||||
```csharp
|
||||
public interface IProductService
|
||||
{
|
||||
Task<ProductDTO> CreateProductAsync(CreateProductDTO dto, string userId);
|
||||
Task<ProductDTO> UpdateProductAsync(int id, UpdateProductDTO dto, string userId);
|
||||
Task DeleteProductAsync(int id, string userId);
|
||||
Task<ProductDTO> GetProductByIdAsync(int id);
|
||||
Task<PagedResult<ProductDTO>> GetProductsPagedAsync(int page, int pageSize, string? search);
|
||||
}
|
||||
```
|
||||
|
||||
### 6B. Implementation (`SeaHaven.Services/Implementation/ProductService.cs`)
|
||||
|
||||
```csharp
|
||||
using FluentValidation;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
|
||||
public class ProductService : IProductService
|
||||
{
|
||||
private readonly IProductDataService _dataService;
|
||||
private readonly IValidator<CreateProductDTO> _createValidator;
|
||||
|
||||
public ProductService(
|
||||
IProductDataService dataService,
|
||||
IValidator<CreateProductDTO> createValidator)
|
||||
{
|
||||
_dataService = dataService;
|
||||
_createValidator = createValidator;
|
||||
}
|
||||
|
||||
public async Task<ProductDTO> CreateProductAsync(CreateProductDTO dto, string userId)
|
||||
{
|
||||
// Validate
|
||||
var validationResult = await _createValidator.ValidateAsync(dto);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ValidationException(validationResult.Errors);
|
||||
}
|
||||
|
||||
// Map to Data DTO
|
||||
var dataDto = new CreateProductDataDTO
|
||||
{
|
||||
Name = dto.Name,
|
||||
Description = dto.Description,
|
||||
Price = dto.Price
|
||||
};
|
||||
|
||||
// Call DataService
|
||||
var created = await _dataService.CreateAsync(dataDto, userId);
|
||||
|
||||
// Map to Service DTO
|
||||
return new ProductDTO
|
||||
{
|
||||
Id = created.Id,
|
||||
Name = created.Name,
|
||||
Description = created.Description,
|
||||
Price = created.Price,
|
||||
IsActive = created.IsActive
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<ProductDTO> GetProductByIdAsync(int id)
|
||||
{
|
||||
var product = await _dataService.GetByIdAsync(id);
|
||||
if (product == null)
|
||||
throw new KeyNotFoundException($"Product with ID {id} not found");
|
||||
|
||||
return new ProductDTO
|
||||
{
|
||||
Id = product.Id,
|
||||
Name = product.Name,
|
||||
Description = product.Description,
|
||||
Price = product.Price,
|
||||
IsActive = product.IsActive
|
||||
};
|
||||
}
|
||||
|
||||
// Implement other methods...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 7: Create Controller
|
||||
|
||||
**File**: `Api.SeaHavenIndustries/Controllers/ProductController.cs`
|
||||
|
||||
```csharp
|
||||
using Api.SeaHavenIndustries.DTOs;
|
||||
using FluentValidation;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
using System.Security.Claims;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class ProductController : Controller
|
||||
{
|
||||
private readonly IProductService _productService;
|
||||
|
||||
public ProductController(IProductService productService)
|
||||
{
|
||||
_productService = productService;
|
||||
}
|
||||
|
||||
[HttpPost("Create")]
|
||||
public async Task<IActionResult> Create([FromBody] Product_DTO dto)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (userId == null)
|
||||
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
||||
|
||||
var serviceDto = new CreateProductDTO
|
||||
{
|
||||
Name = dto.Name,
|
||||
Description = dto.Description,
|
||||
Price = dto.Price
|
||||
};
|
||||
|
||||
await _productService.CreateProductAsync(serviceDto, userId);
|
||||
|
||||
return Ok(new Response { Status = "Success", Message = "Product created successfully" });
|
||||
}
|
||||
catch (ValidationException vex)
|
||||
{
|
||||
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
|
||||
return BadRequest(new Response { Status = "Validation Error", Message = errors });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("GetById")]
|
||||
public async Task<IActionResult> GetById(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var product = await _productService.GetProductByIdAsync(id);
|
||||
return Ok(product);
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
return NotFound(new Response { Status = "Error", Message = "Product not found" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
// Add other endpoints...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 8: Services Auto-Registered ✅
|
||||
|
||||
**No action needed!** Services are automatically registered by assembly scanning.
|
||||
|
||||
If you followed naming conventions (`IProductService` → `ProductService`), your services are already registered.
|
||||
|
||||
**How?** These lines in `Program.cs` scan assemblies:
|
||||
```csharp
|
||||
builder.Services.AddDataServices(); // Scans SeaHaven.DataServices
|
||||
builder.Services.AddBusinessServices(); // Scans SeaHaven.Services + Validators
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 9: Test
|
||||
|
||||
```bash
|
||||
# Build
|
||||
dotnet build
|
||||
|
||||
# Run
|
||||
dotnet run --project Api.SeaHavenIndustries
|
||||
|
||||
# Test endpoint
|
||||
curl -X POST "http://localhost:5141/api/Product/Create" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"Test Product","description":"Test","price":99.99}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Checklist
|
||||
|
||||
- [ ] Entity created with audit fields
|
||||
- [ ] DbSet added to DbContext
|
||||
- [ ] Migration created and applied
|
||||
- [ ] Data DTOs created
|
||||
- [ ] Service DTOs created
|
||||
- [ ] API DTOs created
|
||||
- [ ] FluentValidation validator created
|
||||
- [ ] DataService interface created
|
||||
- [ ] DataService implementation created
|
||||
- [ ] Service interface created
|
||||
- [ ] Service implementation created
|
||||
- [ ] Controller created
|
||||
- [ ] Services registered in DI container
|
||||
- [ ] Exception handling implemented
|
||||
- [ ] User ID authentication added
|
||||
- [ ] Build succeeds
|
||||
- [ ] Endpoints tested
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Common Patterns
|
||||
|
||||
### Get List with Pagination
|
||||
```csharp
|
||||
[HttpGet("GetList")]
|
||||
public async Task<IActionResult> GetList(string? search = "", int page = 1, int pageSize = 10)
|
||||
{
|
||||
var result = await _productService.GetProductsPagedAsync(page, pageSize, search);
|
||||
return Ok(new Pagination_DTO
|
||||
{
|
||||
Data = result.Items,
|
||||
PageNumber = page,
|
||||
PageSize = pageSize,
|
||||
TotalCount = result.TotalCount,
|
||||
TotalPages = (int)Math.Ceiling(result.TotalCount / (double)pageSize)
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Update
|
||||
```csharp
|
||||
[HttpPost("Update")]
|
||||
public async Task<IActionResult> Update([FromBody] EditProduct_DTO dto)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (userId == null)
|
||||
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
||||
|
||||
await _productService.UpdateProductAsync(dto.Id, dto.ToServiceDTO(), userId);
|
||||
return Ok(new Response { Status = "Success", Message = "Updated" });
|
||||
}
|
||||
catch (ValidationException vex)
|
||||
{
|
||||
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
|
||||
return BadRequest(new Response { Status = "Validation Error", Message = errors });
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
return NotFound(new Response { Status = "Error", Message = "Not found" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Delete
|
||||
```csharp
|
||||
[HttpPost("Delete")]
|
||||
public async Task<IActionResult> Delete(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (userId == null)
|
||||
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
||||
|
||||
await _productService.DeleteProductAsync(id, userId);
|
||||
return Ok(new Response { Status = "Success", Message = "Deleted" });
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
return NotFound(new Response { Status = "Error", Message = "Not found" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Reference Implementations
|
||||
|
||||
For complete working examples, see:
|
||||
- `ContactController`, `ContactService`, `ContactDataService`
|
||||
- `EmployeeController`, `EmployeeService`, `EmployeeDataService`
|
||||
- `AssetController`, `AssetService`, `AssetDataService`
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Reflection;
|
||||
|
||||
namespace SeaHaven.DataServices.DependencyInjection
|
||||
{
|
||||
public static class DataServicesModule
|
||||
{
|
||||
private static readonly List<Type> ExcludedTypes = new();
|
||||
|
||||
public static IServiceCollection AddDataServices(this IServiceCollection services)
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
|
||||
var allClasses = assembly.GetTypes()
|
||||
.Where(t => t.IsClass &&
|
||||
!t.IsAbstract &&
|
||||
!t.IsGenericType &&
|
||||
!ExcludedTypes.Contains(t))
|
||||
.ToList();
|
||||
|
||||
foreach (var implementationType in allClasses)
|
||||
{
|
||||
var defaultInterface = implementationType.GetInterfaces()
|
||||
.FirstOrDefault(i => i.Name == $"I{implementationType.Name}");
|
||||
|
||||
if (defaultInterface != null)
|
||||
{
|
||||
services.AddScoped(defaultInterface, implementationType);
|
||||
}
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
75
SeaHaven.DataServices/Implementation/CommentDataService.cs
Normal file
75
SeaHaven.DataServices/Implementation/CommentDataService.cs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
|
||||
namespace SeaHaven.DataServices.Implementation
|
||||
{
|
||||
public class CommentDataService : ICommentDataService
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public CommentDataService(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<Comments?> GetByIdAsync(int id)
|
||||
{
|
||||
return await _context.Comments.FindAsync(id);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Comments>> GetAllAsync()
|
||||
{
|
||||
return await _context.Comments
|
||||
.Include(c => c.ApplicationUser)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Comments>> GetByWorkOrderIdAsync(int workOrderId)
|
||||
{
|
||||
return await _context.Comments
|
||||
.Where(c => c.WorkerOrderId == workOrderId)
|
||||
.Include(c => c.ApplicationUser)
|
||||
.OrderBy(c => c.CreatedDate)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Comments>> GetByDispatchIdAsync(int dispatchId)
|
||||
{
|
||||
return await _context.Comments
|
||||
.Where(c => c.DispatchId == dispatchId)
|
||||
.Include(c => c.ApplicationUser)
|
||||
.OrderBy(c => c.CreatedDate)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<Comments> AddAsync(Comments comment)
|
||||
{
|
||||
comment.CreatedDate = DateTime.UtcNow;
|
||||
await _context.Comments.AddAsync(comment);
|
||||
await _context.SaveChangesAsync();
|
||||
return comment;
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(Comments comment)
|
||||
{
|
||||
_context.Comments.Update(comment);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(int id)
|
||||
{
|
||||
var entity = await GetByIdAsync(id);
|
||||
if (entity != null)
|
||||
{
|
||||
_context.Comments.Remove(entity);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsAsync(int id)
|
||||
{
|
||||
return await _context.Comments.AnyAsync(c => c.Id == id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -111,5 +111,110 @@ namespace SeaHaven.DataServices.Implementation
|
|||
{
|
||||
return await _context.Dispatches.CountAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Dispatch>> GetByWorkOrderIdAsync(int workOrderId)
|
||||
{
|
||||
var dispatchIds = await _context.DispatchWorkOrders
|
||||
.Where(dwo => dwo.WorkOrderId == workOrderId)
|
||||
.Select(dwo => dwo.DispatchId)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
var directIds = await _context.Dispatches
|
||||
.Where(d => d.WorkOrderId == workOrderId)
|
||||
.Select(d => d.Id)
|
||||
.ToListAsync();
|
||||
|
||||
var allIds = dispatchIds.Union(directIds).Distinct().ToList();
|
||||
|
||||
return await _context.Dispatches
|
||||
.Where(d => allIds.Contains(d.Id))
|
||||
.Include(d => d.Vendor)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Dispatch>> GetByWorkOrderIdsAsync(IEnumerable<int> workOrderIds)
|
||||
{
|
||||
return await _context.Dispatches
|
||||
.Where(d => workOrderIds.Contains(d.WorkOrderId ?? 0))
|
||||
.Include(d => d.Vendor)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<string> GenerateNextPONumberAsync()
|
||||
{
|
||||
var lastPO = await _context.Dispatches
|
||||
.Where(d => d.PONumber != null)
|
||||
.OrderByDescending(d => d.PONumber)
|
||||
.Select(d => d.PONumber)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
int nextPO = 1000;
|
||||
if (!string.IsNullOrEmpty(lastPO) && int.TryParse(lastPO, out int lastNum))
|
||||
{
|
||||
nextPO = lastNum + 1;
|
||||
}
|
||||
|
||||
return nextPO.ToString();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<DispatchChecklistItem>> GetChecklistItemsAsync(int dispatchId)
|
||||
{
|
||||
return await _context.DispatchChecklistItems
|
||||
.Where(ci => ci.DispatchId == dispatchId)
|
||||
.OrderBy(ci => ci.SortOrder)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<DispatchChecklistItem?> GetChecklistItemByIdAsync(int id)
|
||||
{
|
||||
return await _context.DispatchChecklistItems.FindAsync(id);
|
||||
}
|
||||
|
||||
public async Task AddChecklistItemAsync(DispatchChecklistItem item)
|
||||
{
|
||||
await _context.DispatchChecklistItems.AddAsync(item);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task UpdateChecklistItemAsync(DispatchChecklistItem item)
|
||||
{
|
||||
_context.DispatchChecklistItems.Update(item);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<DispatchSignoff>> GetSignoffsAsync(int dispatchId)
|
||||
{
|
||||
return await _context.DispatchSignoffs
|
||||
.Where(s => s.DispatchId == dispatchId)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task AddSignoffAsync(DispatchSignoff signoff)
|
||||
{
|
||||
signoff.SignedAt = DateTime.UtcNow;
|
||||
await _context.DispatchSignoffs.AddAsync(signoff);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> HasSignoffTypeAsync(int dispatchId, string signoffType)
|
||||
{
|
||||
return await _context.DispatchSignoffs
|
||||
.AnyAsync(s => s.DispatchId == dispatchId && s.SignoffType == signoffType);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<DispatchWorkOrder>> GetDispatchWorkOrdersAsync(int dispatchId)
|
||||
{
|
||||
return await _context.DispatchWorkOrders
|
||||
.Where(dwo => dwo.DispatchId == dispatchId)
|
||||
.Include(dwo => dwo.WorkOrder)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task AddDispatchWorkOrderAsync(DispatchWorkOrder dispatchWorkOrder)
|
||||
{
|
||||
await _context.DispatchWorkOrders.AddAsync(dispatchWorkOrder);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
75
SeaHaven.DataServices/Implementation/QuotesDataService.cs
Normal file
75
SeaHaven.DataServices/Implementation/QuotesDataService.cs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
|
||||
namespace SeaHaven.DataServices.Implementation
|
||||
{
|
||||
public class QuotesDataService : IQuotesDataService
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public QuotesDataService(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<Quotes?> GetByIdAsync(int id)
|
||||
{
|
||||
return await _context.Quotes.FindAsync(id);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Quotes>> GetAllAsync()
|
||||
{
|
||||
return await _context.Quotes.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Quotes>> GetByWorkOrderIdAsync(int workOrderId)
|
||||
{
|
||||
return await _context.Quotes
|
||||
.Where(q => q.WorkorderId == workOrderId)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<Quotes> AddAsync(Quotes quote)
|
||||
{
|
||||
await _context.Quotes.AddAsync(quote);
|
||||
await _context.SaveChangesAsync();
|
||||
return quote;
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(Quotes quote)
|
||||
{
|
||||
_context.Quotes.Update(quote);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(int id)
|
||||
{
|
||||
var entity = await GetByIdAsync(id);
|
||||
if (entity != null)
|
||||
{
|
||||
_context.Quotes.Remove(entity);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsAsync(int id)
|
||||
{
|
||||
return await _context.Quotes.AnyAsync(q => q.Id == id);
|
||||
}
|
||||
|
||||
public async Task UnlinkFromWorkOrderAsync(int workOrderId)
|
||||
{
|
||||
var quotes = await _context.Quotes
|
||||
.Where(q => q.WorkorderId == workOrderId)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var quote in quotes)
|
||||
{
|
||||
quote.WorkorderId = null;
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
|
||||
namespace SeaHaven.DataServices.Implementation
|
||||
{
|
||||
public class TaskListTemplateDataService : ITaskListTemplateDataService
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public TaskListTemplateDataService(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<TaskListTemplate?> GetByIdAsync(int id)
|
||||
{
|
||||
return await _context.TaskListTemplates.FindAsync(id);
|
||||
}
|
||||
|
||||
public async Task<TaskListTemplate?> GetByIdWithItemsAsync(int id)
|
||||
{
|
||||
return await _context.TaskListTemplates
|
||||
.Include(t => t.Items)
|
||||
.FirstOrDefaultAsync(t => t.Id == id);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<TaskListTemplate>> GetAllAsync()
|
||||
{
|
||||
return await _context.TaskListTemplates.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<TaskListTemplate> AddAsync(TaskListTemplate template)
|
||||
{
|
||||
await _context.TaskListTemplates.AddAsync(template);
|
||||
await _context.SaveChangesAsync();
|
||||
return template;
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(TaskListTemplate template)
|
||||
{
|
||||
_context.TaskListTemplates.Update(template);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(int id)
|
||||
{
|
||||
var entity = await GetByIdAsync(id);
|
||||
if (entity != null)
|
||||
{
|
||||
_context.TaskListTemplates.Remove(entity);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsAsync(int id)
|
||||
{
|
||||
return await _context.TaskListTemplates.AnyAsync(t => t.Id == id);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
SeaHaven.DataServices/Implementation/UserDataService.cs
Normal file
37
SeaHaven.DataServices/Implementation/UserDataService.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
|
||||
namespace SeaHaven.DataServices.Implementation
|
||||
{
|
||||
public class UserDataService : IUserDataService
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public UserDataService(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<ApplicationUser?> GetByIdAsync(string id)
|
||||
{
|
||||
return await _context.Users.FindAsync(id);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ApplicationUser>> GetAllAsync()
|
||||
{
|
||||
return await _context.Users.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<ApplicationUser?> GetByEmailAsync(string email)
|
||||
{
|
||||
return await _context.Users
|
||||
.FirstOrDefaultAsync(u => u.Email == email);
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsAsync(string id)
|
||||
{
|
||||
return await _context.Users.AnyAsync(u => u.Id == id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -239,6 +239,89 @@ namespace SeaHaven.DataServices.Implementation
|
|||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<WorkOrderAuditLog>> GetWorkOrderAuditLogsAsync(int workOrderId)
|
||||
{
|
||||
return await _context.WorkOrderAuditLogs
|
||||
.Where(a => a.WorkOrderId == workOrderId)
|
||||
.OrderByDescending(a => a.CreatedAt)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task AddWorkOrderContactsAsync(int workOrderId, IEnumerable<int> contactIds)
|
||||
{
|
||||
var contacts = contactIds.Select(cid => new WorkOrderContacts
|
||||
{
|
||||
WorkorderId = workOrderId,
|
||||
ContactId = cid
|
||||
});
|
||||
|
||||
await _context.WorkOrderContacts.AddRangeAsync(contacts);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task AddWorkOrderCategoriesAsync(int workOrderId, IEnumerable<int> categoryIds)
|
||||
{
|
||||
var categories = categoryIds.Select(catId => new WorkOrderCategories
|
||||
{
|
||||
WorkorderId = workOrderId,
|
||||
CategoryId = catId
|
||||
});
|
||||
|
||||
await _context.workOrderCategories.AddRangeAsync(categories);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task AddWorkOrderAttachmentAsync(WorkOrderAttachments attachment)
|
||||
{
|
||||
await _context.workOrderAttachments.AddAsync(attachment);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task AddAuditLogAsync(WorkOrderAuditLog auditLog)
|
||||
{
|
||||
await _context.WorkOrderAuditLogs.AddAsync(auditLog);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteWorkOrderContactsAsync(int workOrderId)
|
||||
{
|
||||
var contacts = await _context.WorkOrderContacts
|
||||
.Where(wc => wc.WorkorderId == workOrderId)
|
||||
.ToListAsync();
|
||||
|
||||
if (contacts.Any())
|
||||
{
|
||||
_context.WorkOrderContacts.RemoveRange(contacts);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteWorkOrderCategoriesAsync(int workOrderId)
|
||||
{
|
||||
var categories = await _context.workOrderCategories
|
||||
.Where(wc => wc.WorkorderId == workOrderId)
|
||||
.ToListAsync();
|
||||
|
||||
if (categories.Any())
|
||||
{
|
||||
_context.workOrderCategories.RemoveRange(categories);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteWorkOrderAttachmentsAsync(int workOrderId)
|
||||
{
|
||||
var attachments = await _context.workOrderAttachments
|
||||
.Where(wa => wa.WorkorderId == workOrderId)
|
||||
.ToListAsync();
|
||||
|
||||
if (attachments.Any())
|
||||
{
|
||||
_context.workOrderAttachments.RemoveRange(attachments);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsAsync(int id)
|
||||
{
|
||||
return await _context.workOrders.AnyAsync(w => w.Id == id);
|
||||
|
|
@ -270,5 +353,15 @@ namespace SeaHaven.DataServices.Implementation
|
|||
{
|
||||
return await _context.workOrders.CountAsync(w => w.LocationId == locationId);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<WorkOrder>> GetAllWithDetailsAsync()
|
||||
{
|
||||
return await _context.workOrders
|
||||
.Where(w => w.istemplate != true)
|
||||
.Include(w => w.Locations)
|
||||
.Include(w => w.AssignToUser)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
SeaHaven.DataServices/Interfaces/ICommentDataService.cs
Normal file
16
SeaHaven.DataServices/Interfaces/ICommentDataService.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
|
||||
namespace SeaHaven.DataServices.Interfaces
|
||||
{
|
||||
public interface ICommentDataService
|
||||
{
|
||||
Task<Comments?> GetByIdAsync(int id);
|
||||
Task<IEnumerable<Comments>> GetAllAsync();
|
||||
Task<IEnumerable<Comments>> GetByWorkOrderIdAsync(int workOrderId);
|
||||
Task<IEnumerable<Comments>> GetByDispatchIdAsync(int dispatchId);
|
||||
Task<Comments> AddAsync(Comments comment);
|
||||
Task UpdateAsync(Comments comment);
|
||||
Task DeleteAsync(int id);
|
||||
Task<bool> ExistsAsync(int id);
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,8 @@ namespace SeaHaven.DataServices.Interfaces
|
|||
Task<Dispatch?> GetByIdWithDetailsAsync(int id);
|
||||
Task<IEnumerable<Dispatch>> GetAllAsync();
|
||||
Task<IEnumerable<Dispatch>> GetByVendorIdAsync(int vendorId);
|
||||
Task<IEnumerable<Dispatch>> GetByWorkOrderIdAsync(int workOrderId);
|
||||
Task<IEnumerable<Dispatch>> GetByWorkOrderIdsAsync(IEnumerable<int> workOrderIds);
|
||||
Task<IEnumerable<Dispatch>> GetByStatusAsync(string status);
|
||||
Task<(IEnumerable<Dispatch> Items, int TotalCount)> GetPagedAsync(int page, int pageSize, string? search = null, string? status = null);
|
||||
Task<Dispatch> AddAsync(Dispatch dispatch);
|
||||
|
|
@ -15,6 +17,16 @@ namespace SeaHaven.DataServices.Interfaces
|
|||
Task DeleteAsync(int id);
|
||||
Task<bool> ExistsAsync(int id);
|
||||
Task<string> GenerateNextDispatchNumberAsync();
|
||||
Task<string> GenerateNextPONumberAsync();
|
||||
Task<int> CountAsync();
|
||||
Task<IEnumerable<DispatchChecklistItem>> GetChecklistItemsAsync(int dispatchId);
|
||||
Task<DispatchChecklistItem?> GetChecklistItemByIdAsync(int id);
|
||||
Task AddChecklistItemAsync(DispatchChecklistItem item);
|
||||
Task UpdateChecklistItemAsync(DispatchChecklistItem item);
|
||||
Task<IEnumerable<DispatchSignoff>> GetSignoffsAsync(int dispatchId);
|
||||
Task AddSignoffAsync(DispatchSignoff signoff);
|
||||
Task<bool> HasSignoffTypeAsync(int dispatchId, string signoffType);
|
||||
Task<IEnumerable<DispatchWorkOrder>> GetDispatchWorkOrdersAsync(int dispatchId);
|
||||
Task AddDispatchWorkOrderAsync(DispatchWorkOrder dispatchWorkOrder);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
SeaHaven.DataServices/Interfaces/IQuotesDataService.cs
Normal file
16
SeaHaven.DataServices/Interfaces/IQuotesDataService.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
|
||||
namespace SeaHaven.DataServices.Interfaces
|
||||
{
|
||||
public interface IQuotesDataService
|
||||
{
|
||||
Task<Quotes?> GetByIdAsync(int id);
|
||||
Task<IEnumerable<Quotes>> GetAllAsync();
|
||||
Task<IEnumerable<Quotes>> GetByWorkOrderIdAsync(int workOrderId);
|
||||
Task<Quotes> AddAsync(Quotes quote);
|
||||
Task UpdateAsync(Quotes quote);
|
||||
Task DeleteAsync(int id);
|
||||
Task<bool> ExistsAsync(int id);
|
||||
Task UnlinkFromWorkOrderAsync(int workOrderId);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
|
||||
namespace SeaHaven.DataServices.Interfaces
|
||||
{
|
||||
public interface ITaskListTemplateDataService
|
||||
{
|
||||
Task<TaskListTemplate?> GetByIdAsync(int id);
|
||||
Task<TaskListTemplate?> GetByIdWithItemsAsync(int id);
|
||||
Task<IEnumerable<TaskListTemplate>> GetAllAsync();
|
||||
Task<TaskListTemplate> AddAsync(TaskListTemplate template);
|
||||
Task UpdateAsync(TaskListTemplate template);
|
||||
Task DeleteAsync(int id);
|
||||
Task<bool> ExistsAsync(int id);
|
||||
}
|
||||
}
|
||||
12
SeaHaven.DataServices/Interfaces/IUserDataService.cs
Normal file
12
SeaHaven.DataServices/Interfaces/IUserDataService.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
|
||||
namespace SeaHaven.DataServices.Interfaces
|
||||
{
|
||||
public interface IUserDataService
|
||||
{
|
||||
Task<ApplicationUser?> GetByIdAsync(string id);
|
||||
Task<IEnumerable<ApplicationUser>> GetAllAsync();
|
||||
Task<ApplicationUser?> GetByEmailAsync(string email);
|
||||
Task<bool> ExistsAsync(string id);
|
||||
}
|
||||
}
|
||||
|
|
@ -38,22 +38,35 @@ namespace SeaHaven.DataServices.Interfaces
|
|||
Task UpdateAsync(WorkOrder workOrder);
|
||||
Task DeleteAsync(int id);
|
||||
|
||||
// Related entities
|
||||
// Related entities - Read
|
||||
Task<IEnumerable<WorkOrderContacts>> GetWorkOrderContactsAsync(int workOrderId);
|
||||
Task<IEnumerable<WorkOrderCategories>> GetWorkOrderCategoriesAsync(int workOrderId);
|
||||
Task<IEnumerable<WorkOrderAttachments>> GetWorkOrderAttachmentsAsync(int workOrderId);
|
||||
Task<IEnumerable<Comments>> GetWorkOrderCommentsAsync(int workOrderId);
|
||||
|
||||
Task<IEnumerable<WorkOrderAuditLog>> GetWorkOrderAuditLogsAsync(int workOrderId);
|
||||
|
||||
// Related entities - Write
|
||||
Task AddWorkOrderContactsAsync(int workOrderId, IEnumerable<int> contactIds);
|
||||
Task AddWorkOrderCategoriesAsync(int workOrderId, IEnumerable<int> categoryIds);
|
||||
Task AddWorkOrderAttachmentAsync(WorkOrderAttachments attachment);
|
||||
Task AddAuditLogAsync(WorkOrderAuditLog auditLog);
|
||||
|
||||
// Related entities - Delete
|
||||
Task DeleteWorkOrderContactsAsync(int workOrderId);
|
||||
Task DeleteWorkOrderCategoriesAsync(int workOrderId);
|
||||
Task DeleteWorkOrderAttachmentsAsync(int workOrderId);
|
||||
|
||||
// Validation
|
||||
Task<bool> ExistsAsync(int id);
|
||||
Task<bool> InternalNumberExistsAsync(string internalNumber, int? excludeId = null);
|
||||
|
||||
|
||||
// Internal number generation
|
||||
Task<string> GenerateNextInternalWONumberAsync();
|
||||
|
||||
|
||||
// Statistics
|
||||
Task<int> CountAsync();
|
||||
Task<int> CountByStatusAsync(string status);
|
||||
Task<int> CountByLocationAsync(int locationId);
|
||||
Task<IEnumerable<WorkOrder>> GetAllWithDetailsAsync();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
namespace SeaHaven.Services.DTOs
|
||||
{
|
||||
/// <summary>
|
||||
/// Contact DTO for responses
|
||||
/// </summary>
|
||||
public class ContactDTO
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
|
@ -21,9 +18,6 @@ namespace SeaHaven.Services.DTOs
|
|||
public string? ModifiedBy { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DTO for creating a new contact
|
||||
/// </summary>
|
||||
public class CreateContactDTO
|
||||
{
|
||||
public required string FirstName { get; set; }
|
||||
|
|
@ -36,9 +30,6 @@ namespace SeaHaven.Services.DTOs
|
|||
public int? AccountId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DTO for updating an existing contact
|
||||
/// </summary>
|
||||
public class UpdateContactDTO
|
||||
{
|
||||
public string? FirstName { get; set; }
|
||||
|
|
@ -51,9 +42,6 @@ namespace SeaHaven.Services.DTOs
|
|||
public int? AccountId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Paged result wrapper
|
||||
/// </summary>
|
||||
public class PagedResult<T>
|
||||
{
|
||||
public IEnumerable<T> Items { get; set; } = new List<T>();
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
|
||||
namespace SeaHaven.Services.DTOs
|
||||
{
|
||||
public class WorkOrderDTO
|
||||
|
|
@ -6,8 +9,8 @@ namespace SeaHaven.Services.DTOs
|
|||
public string? InternalWONumber { get; set; }
|
||||
public string? WorkerOrderTitle { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? Status { get; set; }
|
||||
public string? Priority { get; set; }
|
||||
public WorkOrderStatus Status { get; set; }
|
||||
public WorkOrderPriority Priority { get; set; }
|
||||
public int? LocationId { get; set; }
|
||||
public string? LocationName { get; set; }
|
||||
public string? AssignTo { get; set; }
|
||||
|
|
@ -17,28 +20,110 @@ namespace SeaHaven.Services.DTOs
|
|||
public string? CreatedBy { get; set; }
|
||||
public DateTime? ModifiedDate { get; set; }
|
||||
public string? ModifiedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maps entity to DTO (Entity -> DTO)
|
||||
/// </summary>
|
||||
public static WorkOrderDTO MapToModel(WorkOrder entity)
|
||||
{
|
||||
if (entity == null) return null;
|
||||
|
||||
return new WorkOrderDTO
|
||||
{
|
||||
Id = entity.Id,
|
||||
InternalWONumber = entity.InternalWONumber,
|
||||
WorkerOrderTitle = entity.WorkerOrderTitle,
|
||||
Description = entity.Description,
|
||||
Status = Enum.TryParse<WorkOrderStatus>(entity.Status, out var status) ? status : WorkOrderStatus.Open,
|
||||
Priority = Enum.TryParse<WorkOrderPriority>(entity.Priority, out var priority) ? priority : WorkOrderPriority.Medium,
|
||||
LocationId = entity.LocationId,
|
||||
LocationName = null,
|
||||
AssignTo = entity.AssignTo,
|
||||
DueDate = entity.DueDate,
|
||||
CompletedDate = entity.CompletedDate,
|
||||
CreatedDate = entity.CreatedDate,
|
||||
CreatedBy = entity.createdby,
|
||||
ModifiedDate = entity.LastModificationTime,
|
||||
ModifiedBy = entity.LastModifierUserId?.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps DTO to entity (DTO -> Entity)
|
||||
/// </summary>
|
||||
public static WorkOrder MapToEntity(WorkOrderDTO dto)
|
||||
{
|
||||
if (dto == null) return null;
|
||||
|
||||
return new WorkOrder
|
||||
{
|
||||
Id = dto.Id,
|
||||
InternalWONumber = dto.InternalWONumber,
|
||||
WorkerOrderTitle = dto.WorkerOrderTitle,
|
||||
Description = dto.Description,
|
||||
Status = dto.Status.ToString(),
|
||||
Priority = dto.Priority.ToString(),
|
||||
LocationId = dto.LocationId,
|
||||
AssignTo = dto.AssignTo,
|
||||
DueDate = dto.DueDate,
|
||||
CompletedDate = dto.CompletedDate
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class CreateWorkOrderDTO
|
||||
{
|
||||
public required string WorkerOrderTitle { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? Status { get; set; }
|
||||
public string? Priority { get; set; }
|
||||
public WorkOrderStatus? Status { get; set; }
|
||||
public WorkOrderPriority? Priority { get; set; }
|
||||
public int? LocationId { get; set; }
|
||||
public string? AssignTo { get; set; }
|
||||
public DateTime? DueDate { get; set; }
|
||||
|
||||
public static WorkOrder MapToEntity(CreateWorkOrderDTO dto)
|
||||
{
|
||||
if (dto == null) return null!;
|
||||
|
||||
return new WorkOrder
|
||||
{
|
||||
WorkerOrderTitle = dto.WorkerOrderTitle,
|
||||
Description = dto.Description,
|
||||
Status = (dto.Status ?? WorkOrderStatus.Open).ToString(),
|
||||
Priority = (dto.Priority ?? WorkOrderPriority.Medium).ToString(),
|
||||
LocationId = dto.LocationId,
|
||||
AssignTo = dto.AssignTo,
|
||||
DueDate = dto.DueDate,
|
||||
CreatedDate = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class UpdateWorkOrderDTO
|
||||
{
|
||||
public string? WorkerOrderTitle { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? Status { get; set; }
|
||||
public string? Priority { get; set; }
|
||||
public WorkOrderStatus? Status { get; set; }
|
||||
public WorkOrderPriority? Priority { get; set; }
|
||||
public int? LocationId { get; set; }
|
||||
public string? AssignTo { get; set; }
|
||||
public DateTime? DueDate { get; set; }
|
||||
public DateTime? CompletedDate { get; set; }
|
||||
|
||||
public static void UpdateEntity(UpdateWorkOrderDTO dto, WorkOrder entity)
|
||||
{
|
||||
if (dto == null || entity == null) return;
|
||||
|
||||
if (dto.WorkerOrderTitle != null) entity.WorkerOrderTitle = dto.WorkerOrderTitle;
|
||||
if (dto.Description != null) entity.Description = dto.Description;
|
||||
if (dto.Status.HasValue) entity.Status = dto.Status.ToString()!;
|
||||
if (dto.Priority.HasValue) entity.Priority = dto.Priority.ToString()!;
|
||||
if (dto.LocationId.HasValue) entity.LocationId = dto.LocationId;
|
||||
if (dto.AssignTo != null) entity.AssignTo = dto.AssignTo;
|
||||
if (dto.DueDate.HasValue) entity.DueDate = dto.DueDate;
|
||||
if (dto.CompletedDate.HasValue) entity.CompletedDate = dto.CompletedDate;
|
||||
|
||||
entity.LastModificationTime = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
38
SeaHaven.Services/DependencyInjection/ServicesModule.cs
Normal file
38
SeaHaven.Services/DependencyInjection/ServicesModule.cs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
using FluentValidation;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Reflection;
|
||||
|
||||
namespace SeaHaven.Services.DependencyInjection
|
||||
{
|
||||
public static class ServicesModule
|
||||
{
|
||||
private static readonly List<Type> ExcludedTypes = new();
|
||||
|
||||
public static IServiceCollection AddBusinessServices(this IServiceCollection services)
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
|
||||
var allClasses = assembly.GetTypes()
|
||||
.Where(t => t.IsClass &&
|
||||
!t.IsAbstract &&
|
||||
!t.IsGenericType &&
|
||||
!ExcludedTypes.Contains(t))
|
||||
.ToList();
|
||||
|
||||
foreach (var implementationType in allClasses)
|
||||
{
|
||||
var defaultInterface = implementationType.GetInterfaces()
|
||||
.FirstOrDefault(i => i.Name == $"I{implementationType.Name}");
|
||||
|
||||
if (defaultInterface != null)
|
||||
{
|
||||
services.AddScoped(defaultInterface, implementationType);
|
||||
}
|
||||
}
|
||||
|
||||
services.AddValidatorsFromAssembly(assembly);
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using FluentValidation;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
using SeaHaven.Services.Validation;
|
||||
|
||||
namespace SeaHaven.Services.Implementation
|
||||
{
|
||||
|
|
@ -12,10 +14,17 @@ namespace SeaHaven.Services.Implementation
|
|||
public class AccountService : IAccountService
|
||||
{
|
||||
private readonly IAccountDataService _accountDataService;
|
||||
private readonly ICreateAccountValidation _createValidator;
|
||||
private readonly IUpdateAccountValidation _updateValidator;
|
||||
|
||||
public AccountService(IAccountDataService accountDataService)
|
||||
public AccountService(
|
||||
IAccountDataService accountDataService,
|
||||
ICreateAccountValidation createValidator,
|
||||
IUpdateAccountValidation updateValidator)
|
||||
{
|
||||
_accountDataService = accountDataService;
|
||||
_createValidator = createValidator;
|
||||
_updateValidator = updateValidator;
|
||||
}
|
||||
|
||||
public async Task<AccountDTO?> GetAccountByIdAsync(int id)
|
||||
|
|
@ -50,6 +59,12 @@ namespace SeaHaven.Services.Implementation
|
|||
|
||||
public async Task<AccountDTO> CreateAccountAsync(CreateAccountDTO dto, string userId)
|
||||
{
|
||||
var validationResult = await _createValidator.ValidateAsync(dto);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ValidationException(validationResult.Errors);
|
||||
}
|
||||
|
||||
var account = new Accounts
|
||||
{
|
||||
Name = dto.Name,
|
||||
|
|
@ -66,6 +81,12 @@ namespace SeaHaven.Services.Implementation
|
|||
|
||||
public async Task<AccountDTO> UpdateAccountAsync(int id, UpdateAccountDTO dto, string userId)
|
||||
{
|
||||
var validationResult = await _updateValidator.ValidateAsync(dto);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ValidationException(validationResult.Errors);
|
||||
}
|
||||
|
||||
var account = await _accountDataService.GetByIdAsync(id);
|
||||
if (account == null)
|
||||
throw new InvalidOperationException($"Account with ID {id} not found");
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using FluentValidation;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
using SeaHaven.Services.Validation;
|
||||
|
||||
namespace SeaHaven.Services.Implementation
|
||||
{
|
||||
|
|
@ -12,10 +14,17 @@ namespace SeaHaven.Services.Implementation
|
|||
public class AssetService : IAssetService
|
||||
{
|
||||
private readonly IAssetDataService _assetDataService;
|
||||
private readonly ICreateAssetValidation _createValidator;
|
||||
private readonly IUpdateAssetValidation _updateValidator;
|
||||
|
||||
public AssetService(IAssetDataService assetDataService)
|
||||
public AssetService(
|
||||
IAssetDataService assetDataService,
|
||||
ICreateAssetValidation createValidator,
|
||||
IUpdateAssetValidation updateValidator)
|
||||
{
|
||||
_assetDataService = assetDataService;
|
||||
_createValidator = createValidator;
|
||||
_updateValidator = updateValidator;
|
||||
}
|
||||
|
||||
public async Task<AssetDTO?> GetAssetByIdAsync(int id)
|
||||
|
|
@ -56,6 +65,12 @@ namespace SeaHaven.Services.Implementation
|
|||
|
||||
public async Task<AssetDTO> CreateAssetAsync(CreateAssetDTO dto, string userId)
|
||||
{
|
||||
var validationResult = await _createValidator.ValidateAsync(dto);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ValidationException(validationResult.Errors);
|
||||
}
|
||||
|
||||
var asset = new Assets
|
||||
{
|
||||
Name = dto.Name,
|
||||
|
|
@ -70,6 +85,12 @@ namespace SeaHaven.Services.Implementation
|
|||
|
||||
public async Task<AssetDTO> UpdateAssetAsync(int id, UpdateAssetDTO dto, string userId)
|
||||
{
|
||||
var validationResult = await _updateValidator.ValidateAsync(dto);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ValidationException(validationResult.Errors);
|
||||
}
|
||||
|
||||
var asset = await _assetDataService.GetByIdAsync(id);
|
||||
if (asset == null)
|
||||
throw new InvalidOperationException($"Asset with ID {id} not found");
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using FluentValidation;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
using SeaHaven.Services.Validation;
|
||||
|
||||
namespace SeaHaven.Services.Implementation
|
||||
{
|
||||
|
|
@ -12,10 +14,17 @@ namespace SeaHaven.Services.Implementation
|
|||
public class ContactService : IContactService
|
||||
{
|
||||
private readonly IContactDataService _contactDataService;
|
||||
private readonly ICreateContactValidation _createValidator;
|
||||
private readonly IUpdateContactValidation _updateValidator;
|
||||
|
||||
public ContactService(IContactDataService contactDataService)
|
||||
public ContactService(
|
||||
IContactDataService contactDataService,
|
||||
ICreateContactValidation createValidator,
|
||||
IUpdateContactValidation updateValidator)
|
||||
{
|
||||
_contactDataService = contactDataService;
|
||||
_createValidator = createValidator;
|
||||
_updateValidator = updateValidator;
|
||||
}
|
||||
|
||||
// Query operations
|
||||
|
|
@ -76,20 +85,21 @@ namespace SeaHaven.Services.Implementation
|
|||
// Command operations
|
||||
public async Task<ContactDTO> CreateContactAsync(CreateContactDTO dto, string userId)
|
||||
{
|
||||
// Validation
|
||||
if (string.IsNullOrWhiteSpace(dto.Email))
|
||||
throw new ArgumentException("Email is required", nameof(dto.Email));
|
||||
var validationResult = await _createValidator.ValidateAsync(dto);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ValidationException(validationResult.Errors);
|
||||
}
|
||||
|
||||
if (await _contactDataService.EmailExistsAsync(dto.Email))
|
||||
throw new InvalidOperationException($"Email '{dto.Email}' already exists");
|
||||
|
||||
// Map DTO to entity
|
||||
var contact = new Contacts
|
||||
{
|
||||
FirstName = dto.FirstName,
|
||||
LastName = dto.LastName,
|
||||
Email = dto.Email,
|
||||
PhoneNumber = dto.Phone ?? dto.Mobile, // Contacts only has PhoneNumber, not separate Mobile
|
||||
PhoneNumber = dto.Phone ?? dto.Mobile,
|
||||
Title = dto.Title,
|
||||
ContactType = dto.Type,
|
||||
AccountId = dto.AccountId,
|
||||
|
|
@ -97,7 +107,6 @@ namespace SeaHaven.Services.Implementation
|
|||
CreatedDate = DateTime.Now
|
||||
};
|
||||
|
||||
// Save
|
||||
var savedContact = await _contactDataService.AddAsync(contact);
|
||||
|
||||
return MapToDTO(savedContact);
|
||||
|
|
@ -105,19 +114,22 @@ namespace SeaHaven.Services.Implementation
|
|||
|
||||
public async Task<ContactDTO> UpdateContactAsync(int id, UpdateContactDTO dto, string userId)
|
||||
{
|
||||
// Check if exists
|
||||
var validationResult = await _updateValidator.ValidateAsync(dto);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ValidationException(validationResult.Errors);
|
||||
}
|
||||
|
||||
var existingContact = await _contactDataService.GetByIdAsync(id);
|
||||
if (existingContact == null)
|
||||
throw new KeyNotFoundException($"Contact with ID {id} not found");
|
||||
|
||||
// Validate email uniqueness if changed
|
||||
if (!string.IsNullOrWhiteSpace(dto.Email) && dto.Email != existingContact.Email)
|
||||
{
|
||||
if (await _contactDataService.EmailExistsAsync(dto.Email, id))
|
||||
throw new InvalidOperationException($"Email '{dto.Email}' already exists");
|
||||
}
|
||||
|
||||
// Update fields
|
||||
if (!string.IsNullOrWhiteSpace(dto.FirstName))
|
||||
existingContact.FirstName = dto.FirstName;
|
||||
|
||||
|
|
@ -128,7 +140,7 @@ namespace SeaHaven.Services.Implementation
|
|||
existingContact.Email = dto.Email;
|
||||
|
||||
if (dto.Phone != null || dto.Mobile != null)
|
||||
existingContact.PhoneNumber = dto.Phone ?? dto.Mobile; // Contacts only has PhoneNumber
|
||||
existingContact.PhoneNumber = dto.Phone ?? dto.Mobile;
|
||||
|
||||
if (dto.Title != null)
|
||||
existingContact.Title = dto.Title;
|
||||
|
|
@ -141,7 +153,6 @@ namespace SeaHaven.Services.Implementation
|
|||
|
||||
existingContact.LastModificationTime = DateTime.Now;
|
||||
|
||||
// Save
|
||||
await _contactDataService.UpdateAsync(existingContact);
|
||||
|
||||
return MapToDTO(existingContact);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using FluentValidation;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
using SeaHaven.Services.Validation;
|
||||
|
||||
namespace SeaHaven.Services.Implementation
|
||||
{
|
||||
|
|
@ -12,10 +14,17 @@ namespace SeaHaven.Services.Implementation
|
|||
public class DispatchService : IDispatchService
|
||||
{
|
||||
private readonly IDispatchDataService _dispatchDataService;
|
||||
private readonly ICreateDispatchValidation _createValidator;
|
||||
private readonly IUpdateDispatchValidation _updateValidator;
|
||||
|
||||
public DispatchService(IDispatchDataService dispatchDataService)
|
||||
public DispatchService(
|
||||
IDispatchDataService dispatchDataService,
|
||||
ICreateDispatchValidation createValidator,
|
||||
IUpdateDispatchValidation updateValidator)
|
||||
{
|
||||
_dispatchDataService = dispatchDataService;
|
||||
_createValidator = createValidator;
|
||||
_updateValidator = updateValidator;
|
||||
}
|
||||
|
||||
public async Task<DispatchDTO?> GetDispatchByIdAsync(int id)
|
||||
|
|
@ -56,6 +65,12 @@ namespace SeaHaven.Services.Implementation
|
|||
|
||||
public async Task<DispatchDTO> CreateDispatchAsync(CreateDispatchDTO dto, string userId)
|
||||
{
|
||||
var validationResult = await _createValidator.ValidateAsync(dto);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ValidationException(validationResult.Errors);
|
||||
}
|
||||
|
||||
var dispatchNumber = await GenerateNextDispatchNumberAsync();
|
||||
|
||||
var dispatch = new Dispatch
|
||||
|
|
@ -74,6 +89,12 @@ namespace SeaHaven.Services.Implementation
|
|||
|
||||
public async Task<DispatchDTO> UpdateDispatchAsync(int id, UpdateDispatchDTO dto, string userId)
|
||||
{
|
||||
var validationResult = await _updateValidator.ValidateAsync(dto);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ValidationException(validationResult.Errors);
|
||||
}
|
||||
|
||||
var dispatch = await _dispatchDataService.GetByIdAsync(id);
|
||||
if (dispatch == null)
|
||||
throw new InvalidOperationException($"Dispatch with ID {id} not found");
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using FluentValidation;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
using SeaHaven.Services.Validation;
|
||||
|
||||
namespace SeaHaven.Services.Implementation
|
||||
{
|
||||
|
|
@ -12,10 +14,17 @@ namespace SeaHaven.Services.Implementation
|
|||
public class EmployeeService : IEmployeeService
|
||||
{
|
||||
private readonly IEmployeeDataService _employeeDataService;
|
||||
private readonly ICreateEmployeeValidation _createValidator;
|
||||
private readonly IUpdateEmployeeValidation _updateValidator;
|
||||
|
||||
public EmployeeService(IEmployeeDataService employeeDataService)
|
||||
public EmployeeService(
|
||||
IEmployeeDataService employeeDataService,
|
||||
ICreateEmployeeValidation createValidator,
|
||||
IUpdateEmployeeValidation updateValidator)
|
||||
{
|
||||
_employeeDataService = employeeDataService;
|
||||
_createValidator = createValidator;
|
||||
_updateValidator = updateValidator;
|
||||
}
|
||||
|
||||
// Query operations
|
||||
|
|
@ -51,6 +60,12 @@ namespace SeaHaven.Services.Implementation
|
|||
|
||||
public async Task<EmployeeDTO> CreateEmployeeAsync(CreateEmployeeDTO dto, string userId)
|
||||
{
|
||||
var validationResult = await _createValidator.ValidateAsync(dto);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ValidationException(validationResult.Errors);
|
||||
}
|
||||
|
||||
var employee = new Employee
|
||||
{
|
||||
FirstName = dto.FirstName,
|
||||
|
|
@ -86,6 +101,12 @@ namespace SeaHaven.Services.Implementation
|
|||
|
||||
public async Task<EmployeeDTO> UpdateEmployeeAsync(int id, UpdateEmployeeDTO dto, string userId)
|
||||
{
|
||||
var validationResult = await _updateValidator.ValidateAsync(dto);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ValidationException(validationResult.Errors);
|
||||
}
|
||||
|
||||
var employee = await _employeeDataService.GetByIdAsync(id);
|
||||
if (employee == null)
|
||||
throw new InvalidOperationException($"Employee with ID {id} not found");
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using FluentValidation;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
using SeaHaven.Services.Validation;
|
||||
|
||||
namespace SeaHaven.Services.Implementation
|
||||
{
|
||||
|
|
@ -12,10 +14,17 @@ namespace SeaHaven.Services.Implementation
|
|||
public class FollowUpService : IFollowUpService
|
||||
{
|
||||
private readonly IFollowUpDataService _followUpDataService;
|
||||
private readonly ICreateFollowUpValidation _createValidator;
|
||||
private readonly IUpdateFollowUpValidation _updateValidator;
|
||||
|
||||
public FollowUpService(IFollowUpDataService followUpDataService)
|
||||
public FollowUpService(
|
||||
IFollowUpDataService followUpDataService,
|
||||
ICreateFollowUpValidation createValidator,
|
||||
IUpdateFollowUpValidation updateValidator)
|
||||
{
|
||||
_followUpDataService = followUpDataService;
|
||||
_createValidator = createValidator;
|
||||
_updateValidator = updateValidator;
|
||||
}
|
||||
|
||||
public async Task<FollowUpDTO?> GetFollowUpByIdAsync(int id)
|
||||
|
|
@ -63,6 +72,12 @@ namespace SeaHaven.Services.Implementation
|
|||
|
||||
public async Task<FollowUpDTO> CreateFollowUpAsync(CreateFollowUpDTO dto, string userId)
|
||||
{
|
||||
var validationResult = await _createValidator.ValidateAsync(dto);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ValidationException(validationResult.Errors);
|
||||
}
|
||||
|
||||
var followUp = new FollowUps
|
||||
{
|
||||
Reason = dto.Reason,
|
||||
|
|
@ -83,6 +98,12 @@ namespace SeaHaven.Services.Implementation
|
|||
|
||||
public async Task<FollowUpDTO> UpdateFollowUpAsync(int id, UpdateFollowUpDTO dto, string userId)
|
||||
{
|
||||
var validationResult = await _updateValidator.ValidateAsync(dto);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ValidationException(validationResult.Errors);
|
||||
}
|
||||
|
||||
var followUp = await _followUpDataService.GetByIdAsync(id);
|
||||
if (followUp == null)
|
||||
throw new InvalidOperationException($"Follow-Up with ID {id} not found");
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using FluentValidation;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
using SeaHaven.Services.Validation;
|
||||
|
||||
namespace SeaHaven.Services.Implementation
|
||||
{
|
||||
|
|
@ -12,10 +14,17 @@ namespace SeaHaven.Services.Implementation
|
|||
public class LocationService : ILocationService
|
||||
{
|
||||
private readonly ILocationDataService _locationDataService;
|
||||
private readonly ICreateLocationValidation _createValidator;
|
||||
private readonly IUpdateLocationValidation _updateValidator;
|
||||
|
||||
public LocationService(ILocationDataService locationDataService)
|
||||
public LocationService(
|
||||
ILocationDataService locationDataService,
|
||||
ICreateLocationValidation createValidator,
|
||||
IUpdateLocationValidation updateValidator)
|
||||
{
|
||||
_locationDataService = locationDataService;
|
||||
_createValidator = createValidator;
|
||||
_updateValidator = updateValidator;
|
||||
}
|
||||
|
||||
// Query operations
|
||||
|
|
@ -57,6 +66,12 @@ namespace SeaHaven.Services.Implementation
|
|||
|
||||
public async Task<LocationDTO> CreateLocationAsync(CreateLocationDTO dto, string userId)
|
||||
{
|
||||
var validationResult = await _createValidator.ValidateAsync(dto);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ValidationException(validationResult.Errors);
|
||||
}
|
||||
|
||||
var location = new Locations
|
||||
{
|
||||
Name = dto.LocationName,
|
||||
|
|
@ -75,6 +90,12 @@ namespace SeaHaven.Services.Implementation
|
|||
|
||||
public async Task<LocationDTO> UpdateLocationAsync(int id, UpdateLocationDTO dto, string userId)
|
||||
{
|
||||
var validationResult = await _updateValidator.ValidateAsync(dto);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ValidationException(validationResult.Errors);
|
||||
}
|
||||
|
||||
var location = await _locationDataService.GetByIdAsync(id);
|
||||
if (location == null)
|
||||
throw new InvalidOperationException($"Location with ID {id} not found");
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using FluentValidation;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
using SeaHaven.Services.Validation;
|
||||
|
||||
namespace SeaHaven.Services.Implementation
|
||||
{
|
||||
|
|
@ -12,10 +14,17 @@ namespace SeaHaven.Services.Implementation
|
|||
public class PMScheduleService : IPMScheduleService
|
||||
{
|
||||
private readonly IPMScheduleDataService _pmScheduleDataService;
|
||||
private readonly ICreatePMScheduleValidation _createValidator;
|
||||
private readonly IUpdatePMScheduleValidation _updateValidator;
|
||||
|
||||
public PMScheduleService(IPMScheduleDataService pmScheduleDataService)
|
||||
public PMScheduleService(
|
||||
IPMScheduleDataService pmScheduleDataService,
|
||||
ICreatePMScheduleValidation createValidator,
|
||||
IUpdatePMScheduleValidation updateValidator)
|
||||
{
|
||||
_pmScheduleDataService = pmScheduleDataService;
|
||||
_createValidator = createValidator;
|
||||
_updateValidator = updateValidator;
|
||||
}
|
||||
|
||||
public async Task<PMScheduleDTO?> GetPMScheduleByIdAsync(int id)
|
||||
|
|
@ -56,6 +65,12 @@ namespace SeaHaven.Services.Implementation
|
|||
|
||||
public async Task<PMScheduleDTO> CreatePMScheduleAsync(CreatePMScheduleDTO dto, string userId)
|
||||
{
|
||||
var validationResult = await _createValidator.ValidateAsync(dto);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ValidationException(validationResult.Errors);
|
||||
}
|
||||
|
||||
var pmSchedule = new PMSchedules
|
||||
{
|
||||
Name = dto.Name,
|
||||
|
|
@ -74,6 +89,12 @@ namespace SeaHaven.Services.Implementation
|
|||
|
||||
public async Task<PMScheduleDTO> UpdatePMScheduleAsync(int id, UpdatePMScheduleDTO dto, string userId)
|
||||
{
|
||||
var validationResult = await _updateValidator.ValidateAsync(dto);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ValidationException(validationResult.Errors);
|
||||
}
|
||||
|
||||
var pmSchedule = await _pmScheduleDataService.GetByIdAsync(id);
|
||||
if (pmSchedule == null)
|
||||
throw new InvalidOperationException($"PM Schedule with ID {id} not found");
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using FluentValidation;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
using SeaHaven.Services.Validation;
|
||||
|
||||
namespace SeaHaven.Services.Implementation
|
||||
{
|
||||
|
|
@ -12,10 +14,17 @@ namespace SeaHaven.Services.Implementation
|
|||
public class VendorService : IVendorService
|
||||
{
|
||||
private readonly IVendorDataService _vendorDataService;
|
||||
private readonly ICreateVendorValidation _createValidator;
|
||||
private readonly IUpdateVendorValidation _updateValidator;
|
||||
|
||||
public VendorService(IVendorDataService vendorDataService)
|
||||
public VendorService(
|
||||
IVendorDataService vendorDataService,
|
||||
ICreateVendorValidation createValidator,
|
||||
IUpdateVendorValidation updateValidator)
|
||||
{
|
||||
_vendorDataService = vendorDataService;
|
||||
_createValidator = createValidator;
|
||||
_updateValidator = updateValidator;
|
||||
}
|
||||
|
||||
public async Task<VendorDTO?> GetVendorByIdAsync(int id)
|
||||
|
|
@ -50,6 +59,12 @@ namespace SeaHaven.Services.Implementation
|
|||
|
||||
public async Task<VendorDTO> CreateVendorAsync(CreateVendorDTO dto, string userId)
|
||||
{
|
||||
var validationResult = await _createValidator.ValidateAsync(dto);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ValidationException(validationResult.Errors);
|
||||
}
|
||||
|
||||
var vendor = new Vendor
|
||||
{
|
||||
CompanyName = dto.Name,
|
||||
|
|
@ -69,6 +84,12 @@ namespace SeaHaven.Services.Implementation
|
|||
|
||||
public async Task<VendorDTO> UpdateVendorAsync(int id, UpdateVendorDTO dto, string userId)
|
||||
{
|
||||
var validationResult = await _updateValidator.ValidateAsync(dto);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ValidationException(validationResult.Errors);
|
||||
}
|
||||
|
||||
var vendor = await _vendorDataService.GetByIdAsync(id);
|
||||
if (vendor == null)
|
||||
throw new InvalidOperationException($"Vendor with ID {id} not found");
|
||||
|
|
|
|||
|
|
@ -1,35 +1,45 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
using FluentValidation;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
using SeaHaven.Services.Validation;
|
||||
|
||||
namespace SeaHaven.Services.Implementation
|
||||
{
|
||||
public class WorkOrderService : IWorkOrderService
|
||||
{
|
||||
private readonly IWorkOrderDataService _workOrderDataService;
|
||||
private readonly ICreateWorkOrderValidation _createValidator;
|
||||
private readonly IUpdateWorkOrderValidation _updateValidator;
|
||||
|
||||
public WorkOrderService(IWorkOrderDataService workOrderDataService)
|
||||
public WorkOrderService(
|
||||
IWorkOrderDataService workOrderDataService,
|
||||
ICreateWorkOrderValidation createValidator,
|
||||
IUpdateWorkOrderValidation updateValidator)
|
||||
{
|
||||
_workOrderDataService = workOrderDataService;
|
||||
_createValidator = createValidator;
|
||||
_updateValidator = updateValidator;
|
||||
}
|
||||
|
||||
public async Task<WorkOrderDTO?> GetWorkOrderByIdAsync(int id)
|
||||
{
|
||||
var workOrder = await _workOrderDataService.GetByIdAsync(id);
|
||||
return workOrder == null ? null : MapToDTO(workOrder);
|
||||
return workOrder == null ? null : WorkOrderDTO.MapToModel(workOrder);
|
||||
}
|
||||
|
||||
public async Task<WorkOrderDTO?> GetWorkOrderByIdWithDetailsAsync(int id)
|
||||
{
|
||||
var workOrder = await _workOrderDataService.GetByIdWithDetailsAsync(id);
|
||||
return workOrder == null ? null : MapToDTO(workOrder);
|
||||
return workOrder == null ? null : WorkOrderDTO.MapToModel(workOrder);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<WorkOrderDTO>> GetAllWorkOrdersAsync()
|
||||
{
|
||||
var workOrders = await _workOrderDataService.GetAllAsync();
|
||||
return workOrders.Select(MapToDTO);
|
||||
return workOrders.Select(WorkOrderDTO.MapToModel);
|
||||
}
|
||||
|
||||
public async Task<PagedResult<WorkOrderDTO>> GetWorkOrdersPagedAsync(int page, int pageSize, string? search = null, string? status = null, string? priority = null)
|
||||
|
|
@ -37,69 +47,71 @@ namespace SeaHaven.Services.Implementation
|
|||
var (items, totalCount) = await _workOrderDataService.GetPagedAsync(page, pageSize, search, status, priority);
|
||||
return new PagedResult<WorkOrderDTO>
|
||||
{
|
||||
Items = items.Select(MapToDTO),
|
||||
Items = items.Select(WorkOrderDTO.MapToModel),
|
||||
TotalCount = totalCount,
|
||||
Page = page,
|
||||
PageSize = pageSize
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<WorkOrderDTO> CreateWorkOrderAsync(CreateWorkOrderDTO dto, string userId)
|
||||
public async Task<int> CreateWorkOrderAsync(CreateWorkOrderDTO dto, string userId)
|
||||
{
|
||||
var workOrder = new WorkOrder
|
||||
var validationResult = await _createValidator.ValidateAsync(dto);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
WorkerOrderTitle = dto.WorkerOrderTitle,
|
||||
Description = dto.Description,
|
||||
Status = dto.Status ?? "Open",
|
||||
Priority = dto.Priority,
|
||||
LocationId = dto.LocationId,
|
||||
AssignTo = dto.AssignTo,
|
||||
DueDate = dto.DueDate,
|
||||
createdby = userId,
|
||||
CreatedDate = DateTime.Now
|
||||
};
|
||||
throw new ValidationException(validationResult.Errors);
|
||||
}
|
||||
|
||||
// Generate internal number
|
||||
var workOrder = CreateWorkOrderDTO.MapToEntity(dto);
|
||||
workOrder.createdby = userId;
|
||||
workOrder.CreatedDate = DateTime.UtcNow;
|
||||
workOrder.InternalWONumber = await _workOrderDataService.GenerateNextInternalWONumberAsync();
|
||||
|
||||
var savedWorkOrder = await _workOrderDataService.AddAsync(workOrder);
|
||||
return MapToDTO(savedWorkOrder);
|
||||
var saved = await _workOrderDataService.AddAsync(workOrder);
|
||||
|
||||
await LogAuditAsync(saved.Id, userId, AuditAction.Create, "Work order created");
|
||||
|
||||
return saved.Id;
|
||||
}
|
||||
|
||||
public async Task<WorkOrderDTO> UpdateWorkOrderAsync(int id, UpdateWorkOrderDTO dto, string userId)
|
||||
public async Task<bool> UpdateWorkOrderAsync(int id, UpdateWorkOrderDTO dto, string userId)
|
||||
{
|
||||
var validationResult = await _updateValidator.ValidateAsync(dto);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ValidationException(validationResult.Errors);
|
||||
}
|
||||
|
||||
var existingWorkOrder = await _workOrderDataService.GetByIdAsync(id);
|
||||
if (existingWorkOrder == null)
|
||||
throw new KeyNotFoundException($"WorkOrder with ID {id} not found");
|
||||
return false;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(dto.WorkerOrderTitle))
|
||||
existingWorkOrder.WorkerOrderTitle = dto.WorkerOrderTitle;
|
||||
if (dto.Description != null)
|
||||
existingWorkOrder.Description = dto.Description;
|
||||
if (dto.Status != null)
|
||||
existingWorkOrder.Status = dto.Status;
|
||||
if (dto.Priority != null)
|
||||
existingWorkOrder.Priority = dto.Priority;
|
||||
if (dto.LocationId.HasValue)
|
||||
existingWorkOrder.LocationId = dto.LocationId;
|
||||
if (dto.AssignTo != null)
|
||||
existingWorkOrder.AssignTo = dto.AssignTo;
|
||||
if (dto.DueDate.HasValue)
|
||||
existingWorkOrder.DueDate = dto.DueDate;
|
||||
if (dto.CompletedDate.HasValue)
|
||||
existingWorkOrder.CompletedDate = dto.CompletedDate;
|
||||
|
||||
existingWorkOrder.LastModificationTime = DateTime.Now;
|
||||
var changes = new List<string>();
|
||||
changes.AddRange(TrackChanges(existingWorkOrder, dto));
|
||||
|
||||
UpdateWorkOrderDTO.UpdateEntity(dto, existingWorkOrder);
|
||||
await _workOrderDataService.UpdateAsync(existingWorkOrder);
|
||||
return MapToDTO(existingWorkOrder);
|
||||
|
||||
if (changes.Any())
|
||||
{
|
||||
await LogAuditAsync(id, userId, AuditAction.Edit, string.Join(", ", changes));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task DeleteWorkOrderAsync(int id, string userId)
|
||||
public async Task<bool> DeleteWorkOrderAsync(int id, string userId)
|
||||
{
|
||||
if (!await _workOrderDataService.ExistsAsync(id))
|
||||
throw new KeyNotFoundException($"WorkOrder with ID {id} not found");
|
||||
return false;
|
||||
|
||||
await _workOrderDataService.DeleteWorkOrderAttachmentsAsync(id);
|
||||
await _workOrderDataService.DeleteWorkOrderContactsAsync(id);
|
||||
await _workOrderDataService.DeleteWorkOrderCategoriesAsync(id);
|
||||
await _workOrderDataService.DeleteAsync(id);
|
||||
|
||||
await LogAuditAsync(id, userId, AuditAction.Delete, "Work order deleted");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> WorkOrderExistsAsync(int id)
|
||||
|
|
@ -117,26 +129,40 @@ namespace SeaHaven.Services.Implementation
|
|||
return await _workOrderDataService.CountAsync();
|
||||
}
|
||||
|
||||
private static WorkOrderDTO MapToDTO(WorkOrder workOrder)
|
||||
private List<string> TrackChanges(WorkOrder existing, UpdateWorkOrderDTO dto)
|
||||
{
|
||||
return new WorkOrderDTO
|
||||
var changes = new List<string>();
|
||||
|
||||
if (dto.WorkerOrderTitle != null && dto.WorkerOrderTitle != existing.WorkerOrderTitle)
|
||||
changes.Add($"Title: {existing.WorkerOrderTitle} → {dto.WorkerOrderTitle}");
|
||||
|
||||
if (dto.Status.HasValue && dto.Status.ToString() != existing.Status)
|
||||
changes.Add($"Status: {existing.Status} → {dto.Status}");
|
||||
|
||||
if (dto.Priority.HasValue && dto.Priority.ToString() != existing.Priority)
|
||||
changes.Add($"Priority: {existing.Priority} → {dto.Priority}");
|
||||
|
||||
if (dto.AssignTo != null && dto.AssignTo != existing.AssignTo)
|
||||
changes.Add($"Assigned To: {existing.AssignTo} → {dto.AssignTo}");
|
||||
|
||||
if (dto.DueDate.HasValue && dto.DueDate != existing.DueDate)
|
||||
changes.Add($"Due Date: {existing.DueDate:yyyy-MM-dd} → {dto.DueDate:yyyy-MM-dd}");
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
private async Task LogAuditAsync(int workOrderId, string userId, AuditAction action, string description)
|
||||
{
|
||||
var auditLog = new WorkOrderAuditLog
|
||||
{
|
||||
Id = workOrder.Id,
|
||||
InternalWONumber = workOrder.InternalWONumber,
|
||||
WorkerOrderTitle = workOrder.WorkerOrderTitle,
|
||||
Description = workOrder.Description,
|
||||
Status = workOrder.Status,
|
||||
Priority = workOrder.Priority,
|
||||
LocationId = workOrder.LocationId,
|
||||
LocationName = workOrder.Locations?.Name,
|
||||
AssignTo = workOrder.AssignTo,
|
||||
DueDate = workOrder.DueDate,
|
||||
CompletedDate = workOrder.CompletedDate,
|
||||
CreatedDate = workOrder.CreatedDate,
|
||||
CreatedBy = workOrder.createdby,
|
||||
ModifiedDate = workOrder.LastModificationTime,
|
||||
ModifiedBy = null // Not tracked in FullAuditEntity
|
||||
WorkOrderId = workOrderId,
|
||||
Action = action.ToString(),
|
||||
FieldName = description,
|
||||
UserId = userId,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
await _workOrderDataService.AddAuditLogAsync(auditLog);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,9 +8,9 @@ namespace SeaHaven.Services.Interfaces
|
|||
Task<WorkOrderDTO?> GetWorkOrderByIdWithDetailsAsync(int id);
|
||||
Task<IEnumerable<WorkOrderDTO>> GetAllWorkOrdersAsync();
|
||||
Task<PagedResult<WorkOrderDTO>> GetWorkOrdersPagedAsync(int page, int pageSize, string? search = null, string? status = null, string? priority = null);
|
||||
Task<WorkOrderDTO> CreateWorkOrderAsync(CreateWorkOrderDTO dto, string userId);
|
||||
Task<WorkOrderDTO> UpdateWorkOrderAsync(int id, UpdateWorkOrderDTO dto, string userId);
|
||||
Task DeleteWorkOrderAsync(int id, string userId);
|
||||
Task<int> CreateWorkOrderAsync(CreateWorkOrderDTO dto, string userId);
|
||||
Task<bool> UpdateWorkOrderAsync(int id, UpdateWorkOrderDTO dto, string userId);
|
||||
Task<bool> DeleteWorkOrderAsync(int id, string userId);
|
||||
Task<bool> WorkOrderExistsAsync(int id);
|
||||
Task<string> GenerateNextInternalWONumberAsync();
|
||||
Task<int> GetTotalWorkOrderCountAsync();
|
||||
|
|
|
|||
|
|
@ -11,4 +11,9 @@
|
|||
<ProjectReference Include="..\Data.SeaHavenIndustries\Data.SeaHavenIndustries.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentValidation" Version="12.1.1" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
57
SeaHaven.Services/Validation/AccountValidation.cs
Normal file
57
SeaHaven.Services/Validation/AccountValidation.cs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
using FluentValidation;
|
||||
using SeaHaven.Services.DTOs;
|
||||
|
||||
namespace SeaHaven.Services.Validation
|
||||
{
|
||||
public interface ICreateAccountValidation : IValidator<CreateAccountDTO>
|
||||
{
|
||||
}
|
||||
|
||||
public class CreateAccountValidation : AbstractValidator<CreateAccountDTO>, ICreateAccountValidation
|
||||
{
|
||||
public CreateAccountValidation()
|
||||
{
|
||||
RuleFor(x => x.Name)
|
||||
.NotEmpty().WithMessage("Account name is required")
|
||||
.MaximumLength(200).WithMessage("Account name cannot exceed 200 characters");
|
||||
|
||||
RuleFor(x => x.Email)
|
||||
.EmailAddress().WithMessage("Invalid email address")
|
||||
.When(x => !string.IsNullOrEmpty(x.Email));
|
||||
|
||||
RuleFor(x => x.Phone)
|
||||
.MaximumLength(20).WithMessage("Phone number cannot exceed 20 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Phone));
|
||||
|
||||
RuleFor(x => x.Website)
|
||||
.MaximumLength(200).WithMessage("Website cannot exceed 200 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Website));
|
||||
}
|
||||
}
|
||||
|
||||
public interface IUpdateAccountValidation : IValidator<UpdateAccountDTO>
|
||||
{
|
||||
}
|
||||
|
||||
public class UpdateAccountValidation : AbstractValidator<UpdateAccountDTO>, IUpdateAccountValidation
|
||||
{
|
||||
public UpdateAccountValidation()
|
||||
{
|
||||
RuleFor(x => x.Name)
|
||||
.MaximumLength(200).WithMessage("Account name cannot exceed 200 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Name));
|
||||
|
||||
RuleFor(x => x.Email)
|
||||
.EmailAddress().WithMessage("Invalid email address")
|
||||
.When(x => !string.IsNullOrEmpty(x.Email));
|
||||
|
||||
RuleFor(x => x.Phone)
|
||||
.MaximumLength(20).WithMessage("Phone number cannot exceed 20 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Phone));
|
||||
|
||||
RuleFor(x => x.Website)
|
||||
.MaximumLength(200).WithMessage("Website cannot exceed 200 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Website));
|
||||
}
|
||||
}
|
||||
}
|
||||
41
SeaHaven.Services/Validation/AssetValidation.cs
Normal file
41
SeaHaven.Services/Validation/AssetValidation.cs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
using FluentValidation;
|
||||
using SeaHaven.Services.DTOs;
|
||||
|
||||
namespace SeaHaven.Services.Validation
|
||||
{
|
||||
public interface ICreateAssetValidation : IValidator<CreateAssetDTO>
|
||||
{
|
||||
}
|
||||
|
||||
public class CreateAssetValidation : AbstractValidator<CreateAssetDTO>, ICreateAssetValidation
|
||||
{
|
||||
public CreateAssetValidation()
|
||||
{
|
||||
RuleFor(x => x.Name)
|
||||
.NotEmpty().WithMessage("Asset name is required")
|
||||
.MaximumLength(200).WithMessage("Asset name cannot exceed 200 characters");
|
||||
|
||||
RuleFor(x => x.LocationId)
|
||||
.GreaterThan(0).WithMessage("Valid location must be selected")
|
||||
.When(x => x.LocationId.HasValue);
|
||||
}
|
||||
}
|
||||
|
||||
public interface IUpdateAssetValidation : IValidator<UpdateAssetDTO>
|
||||
{
|
||||
}
|
||||
|
||||
public class UpdateAssetValidation : AbstractValidator<UpdateAssetDTO>, IUpdateAssetValidation
|
||||
{
|
||||
public UpdateAssetValidation()
|
||||
{
|
||||
RuleFor(x => x.Name)
|
||||
.MaximumLength(200).WithMessage("Asset name cannot exceed 200 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Name));
|
||||
|
||||
RuleFor(x => x.LocationId)
|
||||
.GreaterThan(0).WithMessage("Valid location must be selected")
|
||||
.When(x => x.LocationId.HasValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
73
SeaHaven.Services/Validation/ContactValidation.cs
Normal file
73
SeaHaven.Services/Validation/ContactValidation.cs
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
using FluentValidation;
|
||||
using SeaHaven.Services.DTOs;
|
||||
|
||||
namespace SeaHaven.Services.Validation
|
||||
{
|
||||
public interface ICreateContactValidation : IValidator<CreateContactDTO>
|
||||
{
|
||||
}
|
||||
|
||||
public class CreateContactValidation : AbstractValidator<CreateContactDTO>, ICreateContactValidation
|
||||
{
|
||||
public CreateContactValidation()
|
||||
{
|
||||
RuleFor(x => x.FirstName)
|
||||
.NotEmpty().WithMessage("First name is required")
|
||||
.MaximumLength(100).WithMessage("First name cannot exceed 100 characters");
|
||||
|
||||
RuleFor(x => x.LastName)
|
||||
.NotEmpty().WithMessage("Last name is required")
|
||||
.MaximumLength(100).WithMessage("Last name cannot exceed 100 characters");
|
||||
|
||||
RuleFor(x => x.Email)
|
||||
.NotEmpty().WithMessage("Email is required")
|
||||
.EmailAddress().WithMessage("Invalid email address");
|
||||
|
||||
RuleFor(x => x.Phone)
|
||||
.MaximumLength(20).WithMessage("Phone number cannot exceed 20 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Phone));
|
||||
|
||||
RuleFor(x => x.Mobile)
|
||||
.MaximumLength(20).WithMessage("Mobile number cannot exceed 20 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Mobile));
|
||||
|
||||
RuleFor(x => x.AccountId)
|
||||
.GreaterThan(0).WithMessage("Valid account must be selected")
|
||||
.When(x => x.AccountId.HasValue);
|
||||
}
|
||||
}
|
||||
|
||||
public interface IUpdateContactValidation : IValidator<UpdateContactDTO>
|
||||
{
|
||||
}
|
||||
|
||||
public class UpdateContactValidation : AbstractValidator<UpdateContactDTO>, IUpdateContactValidation
|
||||
{
|
||||
public UpdateContactValidation()
|
||||
{
|
||||
RuleFor(x => x.FirstName)
|
||||
.MaximumLength(100).WithMessage("First name cannot exceed 100 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.FirstName));
|
||||
|
||||
RuleFor(x => x.LastName)
|
||||
.MaximumLength(100).WithMessage("Last name cannot exceed 100 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.LastName));
|
||||
|
||||
RuleFor(x => x.Email)
|
||||
.EmailAddress().WithMessage("Invalid email address")
|
||||
.When(x => !string.IsNullOrEmpty(x.Email));
|
||||
|
||||
RuleFor(x => x.Phone)
|
||||
.MaximumLength(20).WithMessage("Phone number cannot exceed 20 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Phone));
|
||||
|
||||
RuleFor(x => x.Mobile)
|
||||
.MaximumLength(20).WithMessage("Mobile number cannot exceed 20 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Mobile));
|
||||
|
||||
RuleFor(x => x.AccountId)
|
||||
.GreaterThan(0).WithMessage("Valid account must be selected")
|
||||
.When(x => x.AccountId.HasValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
44
SeaHaven.Services/Validation/DispatchValidation.cs
Normal file
44
SeaHaven.Services/Validation/DispatchValidation.cs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
using FluentValidation;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
|
||||
namespace SeaHaven.Services.Validation
|
||||
{
|
||||
public interface ICreateDispatchValidation : IValidator<CreateDispatchDTO>
|
||||
{
|
||||
}
|
||||
|
||||
public class CreateDispatchValidation : AbstractValidator<CreateDispatchDTO>, ICreateDispatchValidation
|
||||
{
|
||||
public CreateDispatchValidation()
|
||||
{
|
||||
RuleFor(x => x.VendorId)
|
||||
.NotEmpty().WithMessage("Vendor is required")
|
||||
.GreaterThan(0).WithMessage("Valid vendor must be selected");
|
||||
|
||||
RuleFor(x => x.ScheduledDate)
|
||||
.GreaterThan(DateTime.Now.AddDays(-1))
|
||||
.When(x => x.ScheduledDate.HasValue)
|
||||
.WithMessage("Scheduled date cannot be in the past");
|
||||
}
|
||||
}
|
||||
|
||||
public interface IUpdateDispatchValidation : IValidator<UpdateDispatchDTO>
|
||||
{
|
||||
}
|
||||
|
||||
public class UpdateDispatchValidation : AbstractValidator<UpdateDispatchDTO>, IUpdateDispatchValidation
|
||||
{
|
||||
public UpdateDispatchValidation()
|
||||
{
|
||||
RuleFor(x => x.VendorId)
|
||||
.GreaterThan(0).WithMessage("Valid vendor must be selected")
|
||||
.When(x => x.VendorId.HasValue);
|
||||
|
||||
RuleFor(x => x.ScheduledDate)
|
||||
.GreaterThan(DateTime.Now.AddDays(-1))
|
||||
.When(x => x.ScheduledDate.HasValue)
|
||||
.WithMessage("Scheduled date cannot be in the past");
|
||||
}
|
||||
}
|
||||
}
|
||||
81
SeaHaven.Services/Validation/EmployeeValidation.cs
Normal file
81
SeaHaven.Services/Validation/EmployeeValidation.cs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
using FluentValidation;
|
||||
using SeaHaven.Services.DTOs;
|
||||
|
||||
namespace SeaHaven.Services.Validation
|
||||
{
|
||||
public interface ICreateEmployeeValidation : IValidator<CreateEmployeeDTO>
|
||||
{
|
||||
}
|
||||
|
||||
public class CreateEmployeeValidation : AbstractValidator<CreateEmployeeDTO>, ICreateEmployeeValidation
|
||||
{
|
||||
public CreateEmployeeValidation()
|
||||
{
|
||||
RuleFor(x => x.FirstName)
|
||||
.NotEmpty().WithMessage("First name is required")
|
||||
.MaximumLength(100).WithMessage("First name cannot exceed 100 characters");
|
||||
|
||||
RuleFor(x => x.LastName)
|
||||
.NotEmpty().WithMessage("Last name is required")
|
||||
.MaximumLength(100).WithMessage("Last name cannot exceed 100 characters");
|
||||
|
||||
RuleFor(x => x.Email)
|
||||
.EmailAddress().WithMessage("Invalid email address")
|
||||
.When(x => !string.IsNullOrEmpty(x.Email));
|
||||
|
||||
RuleFor(x => x.Phone)
|
||||
.MaximumLength(20).WithMessage("Phone number cannot exceed 20 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Phone));
|
||||
|
||||
RuleFor(x => x.DepartmentId)
|
||||
.GreaterThan(0).WithMessage("Valid department must be selected")
|
||||
.When(x => x.DepartmentId.HasValue);
|
||||
|
||||
RuleFor(x => x.JobTitleId)
|
||||
.GreaterThan(0).WithMessage("Valid job title must be selected")
|
||||
.When(x => x.JobTitleId.HasValue);
|
||||
|
||||
RuleFor(x => x.RegionId)
|
||||
.GreaterThan(0).WithMessage("Valid region must be selected")
|
||||
.When(x => x.RegionId.HasValue);
|
||||
}
|
||||
}
|
||||
|
||||
public interface IUpdateEmployeeValidation : IValidator<UpdateEmployeeDTO>
|
||||
{
|
||||
}
|
||||
|
||||
public class UpdateEmployeeValidation : AbstractValidator<UpdateEmployeeDTO>, IUpdateEmployeeValidation
|
||||
{
|
||||
public UpdateEmployeeValidation()
|
||||
{
|
||||
RuleFor(x => x.FirstName)
|
||||
.MaximumLength(100).WithMessage("First name cannot exceed 100 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.FirstName));
|
||||
|
||||
RuleFor(x => x.LastName)
|
||||
.MaximumLength(100).WithMessage("Last name cannot exceed 100 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.LastName));
|
||||
|
||||
RuleFor(x => x.Email)
|
||||
.EmailAddress().WithMessage("Invalid email address")
|
||||
.When(x => !string.IsNullOrEmpty(x.Email));
|
||||
|
||||
RuleFor(x => x.Phone)
|
||||
.MaximumLength(20).WithMessage("Phone number cannot exceed 20 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Phone));
|
||||
|
||||
RuleFor(x => x.DepartmentId)
|
||||
.GreaterThan(0).WithMessage("Valid department must be selected")
|
||||
.When(x => x.DepartmentId.HasValue);
|
||||
|
||||
RuleFor(x => x.JobTitleId)
|
||||
.GreaterThan(0).WithMessage("Valid job title must be selected")
|
||||
.When(x => x.JobTitleId.HasValue);
|
||||
|
||||
RuleFor(x => x.RegionId)
|
||||
.GreaterThan(0).WithMessage("Valid region must be selected")
|
||||
.When(x => x.RegionId.HasValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
78
SeaHaven.Services/Validation/FollowUpValidation.cs
Normal file
78
SeaHaven.Services/Validation/FollowUpValidation.cs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
using FluentValidation;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
|
||||
namespace SeaHaven.Services.Validation
|
||||
{
|
||||
public interface ICreateFollowUpValidation : IValidator<CreateFollowUpDTO>
|
||||
{
|
||||
}
|
||||
|
||||
public class CreateFollowUpValidation : AbstractValidator<CreateFollowUpDTO>, ICreateFollowUpValidation
|
||||
{
|
||||
public CreateFollowUpValidation()
|
||||
{
|
||||
RuleFor(x => x.Reason)
|
||||
.NotEmpty().WithMessage("Reason is required")
|
||||
.MaximumLength(500).WithMessage("Reason cannot exceed 500 characters");
|
||||
|
||||
RuleFor(x => x.Status)
|
||||
.NotEmpty().WithMessage("Status is required");
|
||||
|
||||
RuleFor(x => x.ScheduleDate)
|
||||
.GreaterThan(DateTime.Now.AddDays(-1))
|
||||
.WithMessage("Schedule date cannot be in the past");
|
||||
|
||||
RuleFor(x => x.EmployeeId)
|
||||
.GreaterThan(0).WithMessage("Valid employee must be selected")
|
||||
.When(x => x.EmployeeId.HasValue);
|
||||
|
||||
RuleFor(x => x.WorkOrderId)
|
||||
.GreaterThan(0).WithMessage("Valid work order must be selected")
|
||||
.When(x => x.WorkOrderId.HasValue);
|
||||
|
||||
RuleFor(x => x.AccountId)
|
||||
.GreaterThan(0).WithMessage("Valid account must be selected")
|
||||
.When(x => x.AccountId.HasValue);
|
||||
|
||||
RuleFor(x => x.LocationId)
|
||||
.GreaterThan(0).WithMessage("Valid location must be selected")
|
||||
.When(x => x.LocationId.HasValue);
|
||||
}
|
||||
}
|
||||
|
||||
public interface IUpdateFollowUpValidation : IValidator<UpdateFollowUpDTO>
|
||||
{
|
||||
}
|
||||
|
||||
public class UpdateFollowUpValidation : AbstractValidator<UpdateFollowUpDTO>, IUpdateFollowUpValidation
|
||||
{
|
||||
public UpdateFollowUpValidation()
|
||||
{
|
||||
RuleFor(x => x.Reason)
|
||||
.MaximumLength(500).WithMessage("Reason cannot exceed 500 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Reason));
|
||||
|
||||
RuleFor(x => x.ScheduleDate)
|
||||
.GreaterThan(DateTime.Now.AddDays(-1))
|
||||
.When(x => x.ScheduleDate.HasValue)
|
||||
.WithMessage("Schedule date cannot be in the past");
|
||||
|
||||
RuleFor(x => x.EmployeeId)
|
||||
.GreaterThan(0).WithMessage("Valid employee must be selected")
|
||||
.When(x => x.EmployeeId.HasValue);
|
||||
|
||||
RuleFor(x => x.WorkOrderId)
|
||||
.GreaterThan(0).WithMessage("Valid work order must be selected")
|
||||
.When(x => x.WorkOrderId.HasValue);
|
||||
|
||||
RuleFor(x => x.AccountId)
|
||||
.GreaterThan(0).WithMessage("Valid account must be selected")
|
||||
.When(x => x.AccountId.HasValue);
|
||||
|
||||
RuleFor(x => x.LocationId)
|
||||
.GreaterThan(0).WithMessage("Valid location must be selected")
|
||||
.When(x => x.LocationId.HasValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
73
SeaHaven.Services/Validation/LocationValidation.cs
Normal file
73
SeaHaven.Services/Validation/LocationValidation.cs
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
using FluentValidation;
|
||||
using SeaHaven.Services.DTOs;
|
||||
|
||||
namespace SeaHaven.Services.Validation
|
||||
{
|
||||
public interface ICreateLocationValidation : IValidator<CreateLocationDTO>
|
||||
{
|
||||
}
|
||||
|
||||
public class CreateLocationValidation : AbstractValidator<CreateLocationDTO>, ICreateLocationValidation
|
||||
{
|
||||
public CreateLocationValidation()
|
||||
{
|
||||
RuleFor(x => x.LocationName)
|
||||
.NotEmpty().WithMessage("Location name is required")
|
||||
.MaximumLength(200).WithMessage("Location name cannot exceed 200 characters");
|
||||
|
||||
RuleFor(x => x.Address)
|
||||
.MaximumLength(500).WithMessage("Address cannot exceed 500 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Address));
|
||||
|
||||
RuleFor(x => x.City)
|
||||
.MaximumLength(100).WithMessage("City cannot exceed 100 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.City));
|
||||
|
||||
RuleFor(x => x.State)
|
||||
.MaximumLength(2).WithMessage("State must be 2 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.State));
|
||||
|
||||
RuleFor(x => x.Zipcode)
|
||||
.MaximumLength(10).WithMessage("Zipcode cannot exceed 10 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Zipcode));
|
||||
|
||||
RuleFor(x => x.AccountId)
|
||||
.GreaterThan(0).WithMessage("Valid account must be selected")
|
||||
.When(x => x.AccountId.HasValue);
|
||||
}
|
||||
}
|
||||
|
||||
public interface IUpdateLocationValidation : IValidator<UpdateLocationDTO>
|
||||
{
|
||||
}
|
||||
|
||||
public class UpdateLocationValidation : AbstractValidator<UpdateLocationDTO>, IUpdateLocationValidation
|
||||
{
|
||||
public UpdateLocationValidation()
|
||||
{
|
||||
RuleFor(x => x.LocationName)
|
||||
.MaximumLength(200).WithMessage("Location name cannot exceed 200 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.LocationName));
|
||||
|
||||
RuleFor(x => x.Address)
|
||||
.MaximumLength(500).WithMessage("Address cannot exceed 500 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Address));
|
||||
|
||||
RuleFor(x => x.City)
|
||||
.MaximumLength(100).WithMessage("City cannot exceed 100 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.City));
|
||||
|
||||
RuleFor(x => x.State)
|
||||
.MaximumLength(2).WithMessage("State must be 2 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.State));
|
||||
|
||||
RuleFor(x => x.Zipcode)
|
||||
.MaximumLength(10).WithMessage("Zipcode cannot exceed 10 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Zipcode));
|
||||
|
||||
RuleFor(x => x.AccountId)
|
||||
.GreaterThan(0).WithMessage("Valid account must be selected")
|
||||
.When(x => x.AccountId.HasValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
62
SeaHaven.Services/Validation/PMScheduleValidation.cs
Normal file
62
SeaHaven.Services/Validation/PMScheduleValidation.cs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
using FluentValidation;
|
||||
using SeaHaven.Services.DTOs;
|
||||
|
||||
namespace SeaHaven.Services.Validation
|
||||
{
|
||||
public interface ICreatePMScheduleValidation : IValidator<CreatePMScheduleDTO>
|
||||
{
|
||||
}
|
||||
|
||||
public class CreatePMScheduleValidation : AbstractValidator<CreatePMScheduleDTO>, ICreatePMScheduleValidation
|
||||
{
|
||||
public CreatePMScheduleValidation()
|
||||
{
|
||||
RuleFor(x => x.Name)
|
||||
.NotEmpty().WithMessage("PM Schedule name is required")
|
||||
.MaximumLength(200).WithMessage("Name cannot exceed 200 characters");
|
||||
|
||||
RuleFor(x => x.Frequency)
|
||||
.NotEmpty().WithMessage("Frequency is required");
|
||||
|
||||
RuleFor(x => x.StartDate)
|
||||
.NotEmpty().WithMessage("Start date is required")
|
||||
.LessThanOrEqualTo(DateTime.Now.AddYears(1))
|
||||
.WithMessage("Start date cannot be more than 1 year in the future");
|
||||
|
||||
RuleFor(x => x.AssetId)
|
||||
.GreaterThan(0).WithMessage("Valid asset must be selected")
|
||||
.When(x => x.AssetId.HasValue);
|
||||
|
||||
RuleFor(x => x.LocationId)
|
||||
.GreaterThan(0).WithMessage("Valid location must be selected")
|
||||
.When(x => x.LocationId.HasValue);
|
||||
}
|
||||
}
|
||||
|
||||
public interface IUpdatePMScheduleValidation : IValidator<UpdatePMScheduleDTO>
|
||||
{
|
||||
}
|
||||
|
||||
public class UpdatePMScheduleValidation : AbstractValidator<UpdatePMScheduleDTO>, IUpdatePMScheduleValidation
|
||||
{
|
||||
public UpdatePMScheduleValidation()
|
||||
{
|
||||
RuleFor(x => x.Name)
|
||||
.MaximumLength(200).WithMessage("Name cannot exceed 200 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Name));
|
||||
|
||||
RuleFor(x => x.StartDate)
|
||||
.LessThanOrEqualTo(DateTime.Now.AddYears(1))
|
||||
.When(x => x.StartDate.HasValue)
|
||||
.WithMessage("Start date cannot be more than 1 year in the future");
|
||||
|
||||
RuleFor(x => x.AssetId)
|
||||
.GreaterThan(0).WithMessage("Valid asset must be selected")
|
||||
.When(x => x.AssetId.HasValue);
|
||||
|
||||
RuleFor(x => x.LocationId)
|
||||
.GreaterThan(0).WithMessage("Valid location must be selected")
|
||||
.When(x => x.LocationId.HasValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
81
SeaHaven.Services/Validation/VendorValidation.cs
Normal file
81
SeaHaven.Services/Validation/VendorValidation.cs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
using FluentValidation;
|
||||
using SeaHaven.Services.DTOs;
|
||||
|
||||
namespace SeaHaven.Services.Validation
|
||||
{
|
||||
public interface ICreateVendorValidation : IValidator<CreateVendorDTO>
|
||||
{
|
||||
}
|
||||
|
||||
public class CreateVendorValidation : AbstractValidator<CreateVendorDTO>, ICreateVendorValidation
|
||||
{
|
||||
public CreateVendorValidation()
|
||||
{
|
||||
RuleFor(x => x.Name)
|
||||
.NotEmpty().WithMessage("Vendor name is required")
|
||||
.MaximumLength(200).WithMessage("Vendor name cannot exceed 200 characters");
|
||||
|
||||
RuleFor(x => x.Email)
|
||||
.EmailAddress().WithMessage("Invalid email address")
|
||||
.When(x => !string.IsNullOrEmpty(x.Email));
|
||||
|
||||
RuleFor(x => x.Phone)
|
||||
.MaximumLength(20).WithMessage("Phone number cannot exceed 20 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Phone));
|
||||
|
||||
RuleFor(x => x.Address)
|
||||
.MaximumLength(500).WithMessage("Address cannot exceed 500 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Address));
|
||||
|
||||
RuleFor(x => x.City)
|
||||
.MaximumLength(100).WithMessage("City cannot exceed 100 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.City));
|
||||
|
||||
RuleFor(x => x.State)
|
||||
.MaximumLength(2).WithMessage("State must be 2 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.State));
|
||||
|
||||
RuleFor(x => x.Zipcode)
|
||||
.MaximumLength(10).WithMessage("Zipcode cannot exceed 10 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Zipcode));
|
||||
}
|
||||
}
|
||||
|
||||
public interface IUpdateVendorValidation : IValidator<UpdateVendorDTO>
|
||||
{
|
||||
}
|
||||
|
||||
public class UpdateVendorValidation : AbstractValidator<UpdateVendorDTO>, IUpdateVendorValidation
|
||||
{
|
||||
public UpdateVendorValidation()
|
||||
{
|
||||
RuleFor(x => x.Name)
|
||||
.MaximumLength(200).WithMessage("Vendor name cannot exceed 200 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Name));
|
||||
|
||||
RuleFor(x => x.Email)
|
||||
.EmailAddress().WithMessage("Invalid email address")
|
||||
.When(x => !string.IsNullOrEmpty(x.Email));
|
||||
|
||||
RuleFor(x => x.Phone)
|
||||
.MaximumLength(20).WithMessage("Phone number cannot exceed 20 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Phone));
|
||||
|
||||
RuleFor(x => x.Address)
|
||||
.MaximumLength(500).WithMessage("Address cannot exceed 500 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Address));
|
||||
|
||||
RuleFor(x => x.City)
|
||||
.MaximumLength(100).WithMessage("City cannot exceed 100 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.City));
|
||||
|
||||
RuleFor(x => x.State)
|
||||
.MaximumLength(2).WithMessage("State must be 2 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.State));
|
||||
|
||||
RuleFor(x => x.Zipcode)
|
||||
.MaximumLength(10).WithMessage("Zipcode cannot exceed 10 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Zipcode));
|
||||
}
|
||||
}
|
||||
}
|
||||
71
SeaHaven.Services/Validation/WorkOrderValidation.cs
Normal file
71
SeaHaven.Services/Validation/WorkOrderValidation.cs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
using FluentValidation;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
|
||||
namespace SeaHaven.Services.Validation
|
||||
{
|
||||
public interface ICreateWorkOrderValidation : IValidator<CreateWorkOrderDTO>
|
||||
{
|
||||
}
|
||||
|
||||
public class CreateWorkOrderValidation : AbstractValidator<CreateWorkOrderDTO>, ICreateWorkOrderValidation
|
||||
{
|
||||
public CreateWorkOrderValidation()
|
||||
{
|
||||
RuleFor(x => x.WorkerOrderTitle)
|
||||
.NotEmpty().WithMessage("Work order title is required")
|
||||
.MaximumLength(200).WithMessage("Title cannot exceed 200 characters");
|
||||
|
||||
RuleFor(x => x.Description)
|
||||
.MaximumLength(2000).WithMessage("Description cannot exceed 2000 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Description));
|
||||
|
||||
RuleFor(x => x.LocationId)
|
||||
.GreaterThan(0).WithMessage("Valid location must be selected")
|
||||
.When(x => x.LocationId.HasValue);
|
||||
|
||||
RuleFor(x => x.Priority)
|
||||
.IsInEnum().WithMessage("Invalid priority value")
|
||||
.When(x => x.Priority.HasValue);
|
||||
|
||||
RuleFor(x => x.Status)
|
||||
.IsInEnum().WithMessage("Invalid status value")
|
||||
.When(x => x.Status.HasValue);
|
||||
|
||||
RuleFor(x => x.DueDate)
|
||||
.GreaterThan(DateTime.Now.AddDays(-1))
|
||||
.When(x => x.DueDate.HasValue)
|
||||
.WithMessage("Due date cannot be in the past");
|
||||
}
|
||||
}
|
||||
|
||||
public interface IUpdateWorkOrderValidation : IValidator<UpdateWorkOrderDTO>
|
||||
{
|
||||
}
|
||||
|
||||
public class UpdateWorkOrderValidation : AbstractValidator<UpdateWorkOrderDTO>, IUpdateWorkOrderValidation
|
||||
{
|
||||
public UpdateWorkOrderValidation()
|
||||
{
|
||||
RuleFor(x => x.WorkerOrderTitle)
|
||||
.MaximumLength(200).WithMessage("Title cannot exceed 200 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.WorkerOrderTitle));
|
||||
|
||||
RuleFor(x => x.Description)
|
||||
.MaximumLength(2000).WithMessage("Description cannot exceed 2000 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.Description));
|
||||
|
||||
RuleFor(x => x.LocationId)
|
||||
.GreaterThan(0).WithMessage("Valid location must be selected")
|
||||
.When(x => x.LocationId.HasValue);
|
||||
|
||||
RuleFor(x => x.Priority)
|
||||
.IsInEnum().WithMessage("Invalid priority value")
|
||||
.When(x => x.Priority.HasValue);
|
||||
|
||||
RuleFor(x => x.Status)
|
||||
.IsInEnum().WithMessage("Invalid status value")
|
||||
.When(x => x.Status.HasValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue