mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 06:03:12 +00:00
fixing architecture
This commit is contained in:
parent
59385cf5b1
commit
57d378125f
5 changed files with 0 additions and 1758 deletions
|
|
@ -1,344 +0,0 @@
|
|||
# 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,553 +0,0 @@
|
|||
/*
|
||||
===================================================================================
|
||||
CALENDAR/EVENTS MODULE - COMPLETE IMPLEMENTATION
|
||||
===================================================================================
|
||||
This file contains everything needed for the Calendar/Events module:
|
||||
1. Events Model (Database Entity)
|
||||
2. Event_DTO (for GET responses)
|
||||
3. EditEvent_DTO (for POST/PUT requests)
|
||||
4. CalendarController (API endpoints)
|
||||
5. SQL Script for database table creation and sample data
|
||||
|
||||
Author: AI Agent
|
||||
Date: 2026-03-30
|
||||
===================================================================================
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Api.SeaHavenIndustries.DTOs;
|
||||
using Data.SeaHavenIndustries;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
// ===================================================================================
|
||||
// SECTION 1: DATABASE MODEL (Data.SeaHavenIndustries/Models/Events.cs)
|
||||
// ===================================================================================
|
||||
|
||||
namespace Data.SeaHavenIndustries
|
||||
{
|
||||
public class Events : FullAuditEntity
|
||||
{
|
||||
public string Title { get; set; } = null!;
|
||||
public string? Description { get; set; }
|
||||
public string? Location { get; set; }
|
||||
|
||||
// Related Record (can link to Accounts, Contacts, Work Orders, etc.)
|
||||
public int? RecordID { get; set; }
|
||||
public string? RecordName { get; set; }
|
||||
public string? RecordTargetType { get; set; }
|
||||
|
||||
// Reminder Settings
|
||||
public int ReminderMinutes { get; set; } = 15; // Default: 15 minutes before
|
||||
|
||||
// Color for calendar display
|
||||
public string EventColor { get; set; } = "primary"; // primary, success, info, warning, danger
|
||||
|
||||
// Schedule
|
||||
public DateTime StartDate { get; set; }
|
||||
public string? StartTime { get; set; } // e.g., "08:00 AM"
|
||||
public DateTime EndDate { get; set; }
|
||||
public string? EndTime { get; set; } // e.g., "08:30 AM"
|
||||
public bool AllDay { get; set; } = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// SECTION 2: DTOs (Api.SeaHavenIndustries/DTOs/Event_DTO.cs)
|
||||
// ===================================================================================
|
||||
|
||||
namespace Api.SeaHavenIndustries.DTOs
|
||||
{
|
||||
// DTO for GET responses
|
||||
public class Event_DTO
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Title { get; set; } = null!;
|
||||
public string? Description { get; set; }
|
||||
public string? Location { get; set; }
|
||||
|
||||
public int? RecordID { get; set; }
|
||||
public string? RecordName { get; set; }
|
||||
public string? RecordTargetType { get; set; }
|
||||
|
||||
public int ReminderMinutes { get; set; }
|
||||
public string EventColor { get; set; } = null!;
|
||||
|
||||
public DateTime StartDate { get; set; }
|
||||
public string? StartTime { get; set; }
|
||||
public DateTime EndDate { get; set; }
|
||||
public string? EndTime { get; set; }
|
||||
public bool AllDay { get; set; }
|
||||
|
||||
public DateTime? CreatedDate { get; set; }
|
||||
public string? createdby { get; set; }
|
||||
}
|
||||
|
||||
// DTO for POST/PUT requests (Create/Edit)
|
||||
public class EditEvent_DTO
|
||||
{
|
||||
public int? Id { get; set; } // Null for create, populated for edit
|
||||
|
||||
[Required(ErrorMessage = "Please enter a title")]
|
||||
[StringLength(100, ErrorMessage = "Title cannot be longer than 100 characters")]
|
||||
public string Title { get; set; } = null!;
|
||||
|
||||
public string? Description { get; set; }
|
||||
|
||||
[StringLength(150, ErrorMessage = "Location cannot be longer than 150 characters")]
|
||||
public string? Location { get; set; }
|
||||
|
||||
public int? RecordID { get; set; }
|
||||
public string? RecordName { get; set; }
|
||||
public string? RecordTargetType { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "The Remind Attendees field is required")]
|
||||
public int ReminderMinutes { get; set; } = 15;
|
||||
|
||||
[Required(ErrorMessage = "The Select a Calendar Color field is required")]
|
||||
public string EventColor { get; set; } = "primary";
|
||||
|
||||
[Required(ErrorMessage = "Please select a start date")]
|
||||
public DateTime StartDate { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Please select a start time")]
|
||||
public string StartTime { get; set; } = null!;
|
||||
|
||||
[Required(ErrorMessage = "Please select an end date")]
|
||||
public DateTime EndDate { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Please select an end time")]
|
||||
public string EndTime { get; set; } = null!;
|
||||
|
||||
public bool AllDay { get; set; } = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// SECTION 3: CONTROLLER (Api.SeaHavenIndustries/Controllers/CalendarController.cs)
|
||||
// ===================================================================================
|
||||
|
||||
namespace Api.SeaHavenIndustries.Controllers
|
||||
{
|
||||
//[Authorize] // Uncomment when authentication is needed
|
||||
[ApiController]
|
||||
[Route("api/Calendar")]
|
||||
public class CalendarController : Controller
|
||||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly ApplicationDbContext _db;
|
||||
|
||||
public CalendarController(UserManager<ApplicationUser> userManager, ApplicationDbContext db)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_db = db;
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// CREATE EVENT
|
||||
// ===================================================================================
|
||||
[HttpPost]
|
||||
[Route("AddEvent")]
|
||||
public async Task<IActionResult> AddEvent(EditEvent_DTO eventDto)
|
||||
{
|
||||
try
|
||||
{
|
||||
var model = new Events
|
||||
{
|
||||
Title = eventDto.Title,
|
||||
Description = eventDto.Description,
|
||||
Location = eventDto.Location,
|
||||
RecordID = eventDto.RecordID,
|
||||
RecordName = eventDto.RecordName,
|
||||
RecordTargetType = eventDto.RecordTargetType,
|
||||
ReminderMinutes = eventDto.ReminderMinutes,
|
||||
EventColor = eventDto.EventColor,
|
||||
StartDate = eventDto.StartDate,
|
||||
StartTime = eventDto.StartTime,
|
||||
EndDate = eventDto.EndDate,
|
||||
EndTime = eventDto.EndTime,
|
||||
AllDay = eventDto.AllDay,
|
||||
CreatedDate = DateTime.Now,
|
||||
IsDeleted = false
|
||||
};
|
||||
|
||||
_db.Events.Add(model);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return Ok(new DataResponse { Message = "Event Created Successfully", Status = "200" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// UPDATE EVENT
|
||||
// ===================================================================================
|
||||
[HttpPost]
|
||||
[Route("EditEvent")]
|
||||
public async Task<IActionResult> EditEvent(EditEvent_DTO eventDto)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!eventDto.Id.HasValue)
|
||||
{
|
||||
return BadRequest(new Response { Status = "Error", Message = "Event ID is required for editing" });
|
||||
}
|
||||
|
||||
var model = await _db.Events.FindAsync(eventDto.Id.Value);
|
||||
if (model == null || model.IsDeleted == true)
|
||||
{
|
||||
return NotFound(new Response { Status = "Error", Message = "Event not found" });
|
||||
}
|
||||
|
||||
model.Title = eventDto.Title;
|
||||
model.Description = eventDto.Description;
|
||||
model.Location = eventDto.Location;
|
||||
model.RecordID = eventDto.RecordID;
|
||||
model.RecordName = eventDto.RecordName;
|
||||
model.RecordTargetType = eventDto.RecordTargetType;
|
||||
model.ReminderMinutes = eventDto.ReminderMinutes;
|
||||
model.EventColor = eventDto.EventColor;
|
||||
model.StartDate = eventDto.StartDate;
|
||||
model.StartTime = eventDto.StartTime;
|
||||
model.EndDate = eventDto.EndDate;
|
||||
model.EndTime = eventDto.EndTime;
|
||||
model.AllDay = eventDto.AllDay;
|
||||
model.LastModificationTime = DateTime.Now;
|
||||
|
||||
_db.Events.Update(model);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return Ok(new DataResponse { Message = "Event Updated Successfully", Status = "200" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// GET EVENT BY ID
|
||||
// ===================================================================================
|
||||
[HttpGet]
|
||||
[Route("GetEventById/{id}")]
|
||||
public async Task<IActionResult> GetEventById(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var model = await _db.Events
|
||||
.Where(e => e.Id == id && e.IsDeleted != true)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (model == null)
|
||||
{
|
||||
return NotFound(new Response { Status = "Error", Message = "Event not found" });
|
||||
}
|
||||
|
||||
var dto = new Event_DTO
|
||||
{
|
||||
Id = model.Id,
|
||||
Title = model.Title,
|
||||
Description = model.Description,
|
||||
Location = model.Location,
|
||||
RecordID = model.RecordID,
|
||||
RecordName = model.RecordName,
|
||||
RecordTargetType = model.RecordTargetType,
|
||||
ReminderMinutes = model.ReminderMinutes,
|
||||
EventColor = model.EventColor,
|
||||
StartDate = model.StartDate,
|
||||
StartTime = model.StartTime,
|
||||
EndDate = model.EndDate,
|
||||
EndTime = model.EndTime,
|
||||
AllDay = model.AllDay,
|
||||
CreatedDate = model.CreatedDate,
|
||||
createdby = model.createdby
|
||||
};
|
||||
|
||||
return Ok(new DataResponse { Message = "Success", Status = "200", Data = dto });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ===================================================================================
|
||||
// DELETE EVENT
|
||||
// ===================================================================================
|
||||
[HttpDelete]
|
||||
[Route("DeleteEvent/{id}")]
|
||||
public async Task<IActionResult> DeleteEvent(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var model = await _db.Events.FindAsync(id);
|
||||
if (model == null)
|
||||
{
|
||||
return NotFound(new Response { Status = "Error", Message = "Event not found" });
|
||||
}
|
||||
|
||||
// Soft delete
|
||||
model.IsDeleted = true;
|
||||
model.DeletionTime = DateTime.Now;
|
||||
|
||||
_db.Events.Update(model);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return Ok(new DataResponse { Message = "Event Deleted Successfully", Status = "200" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// GET EVENTS LIST (WITH PAGINATION AND FILTERS)
|
||||
// ===================================================================================
|
||||
[HttpPost]
|
||||
[Route("GetEventList")]
|
||||
public IActionResult GetEventList(Pagination_DTO dto)
|
||||
{
|
||||
try
|
||||
{
|
||||
var query = _db.Events.Where(e => e.IsDeleted != true);
|
||||
|
||||
// Apply search filter if provided
|
||||
if (!string.IsNullOrWhiteSpace(dto.Search))
|
||||
{
|
||||
query = query.Where(e =>
|
||||
e.Title.Contains(dto.Search) ||
|
||||
(e.Description != null && e.Description.Contains(dto.Search)) ||
|
||||
(e.Location != null && e.Location.Contains(dto.Search))
|
||||
);
|
||||
}
|
||||
|
||||
// IMPORTANT: Calculate totalCount AFTER applying filters
|
||||
var totalCount = query.Count();
|
||||
|
||||
// Apply pagination
|
||||
var events = query
|
||||
.OrderByDescending(e => e.StartDate)
|
||||
.ThenByDescending(e => e.CreatedDate)
|
||||
.Skip((dto.PageNumber - 1) * dto.PageSize)
|
||||
.Take(dto.PageSize)
|
||||
.Select(e => new Event_DTO
|
||||
{
|
||||
Id = e.Id,
|
||||
Title = e.Title,
|
||||
Description = e.Description,
|
||||
Location = e.Location,
|
||||
RecordID = e.RecordID,
|
||||
RecordName = e.RecordName,
|
||||
RecordTargetType = e.RecordTargetType,
|
||||
ReminderMinutes = e.ReminderMinutes,
|
||||
EventColor = e.EventColor,
|
||||
StartDate = e.StartDate,
|
||||
StartTime = e.StartTime,
|
||||
EndDate = e.EndDate,
|
||||
EndTime = e.EndTime,
|
||||
AllDay = e.AllDay,
|
||||
CreatedDate = e.CreatedDate,
|
||||
createdby = e.createdby
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return Ok(new DataResponse
|
||||
{
|
||||
Message = "Success",
|
||||
Status = "200",
|
||||
Data = new { rows = events, totalCount }
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// GET EVENTS FOR CALENDAR (Date Range Filter)
|
||||
// ===================================================================================
|
||||
[HttpGet]
|
||||
[Route("GetEventsForCalendar")]
|
||||
public IActionResult GetEventsForCalendar([FromQuery] DateTime? start, [FromQuery] DateTime? end)
|
||||
{
|
||||
try
|
||||
{
|
||||
var query = _db.Events.Where(e => e.IsDeleted != true);
|
||||
|
||||
// Filter by date range if provided
|
||||
if (start.HasValue && end.HasValue)
|
||||
{
|
||||
query = query.Where(e =>
|
||||
(e.StartDate >= start.Value && e.StartDate <= end.Value) ||
|
||||
(e.EndDate >= start.Value && e.EndDate <= end.Value) ||
|
||||
(e.StartDate <= start.Value && e.EndDate >= end.Value)
|
||||
);
|
||||
}
|
||||
|
||||
var events = query
|
||||
.Select(e => new Event_DTO
|
||||
{
|
||||
Id = e.Id,
|
||||
Title = e.Title,
|
||||
Description = e.Description,
|
||||
Location = e.Location,
|
||||
RecordID = e.RecordID,
|
||||
RecordName = e.RecordName,
|
||||
RecordTargetType = e.RecordTargetType,
|
||||
ReminderMinutes = e.ReminderMinutes,
|
||||
EventColor = e.EventColor,
|
||||
StartDate = e.StartDate,
|
||||
StartTime = e.StartTime,
|
||||
EndDate = e.EndDate,
|
||||
EndTime = e.EndTime,
|
||||
AllDay = e.AllDay,
|
||||
CreatedDate = e.CreatedDate,
|
||||
createdby = e.createdby
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return Ok(new DataResponse { Message = "Success", Status = "200", Data = events });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
===================================================================================
|
||||
SECTION 4: DATABASE SETUP
|
||||
===================================================================================
|
||||
|
||||
STEP 1: Add Events DbSet to ApplicationDbContext.cs
|
||||
-------------------------------------------------------
|
||||
Add this line in the ApplicationDbContext class (around line 48):
|
||||
|
||||
public DbSet<Events> Events { get; set; }
|
||||
|
||||
|
||||
STEP 2: Create Database Migration
|
||||
-------------------------------------------------------
|
||||
Run these commands in the terminal from the Api.SeaHavenIndustries project directory:
|
||||
|
||||
dotnet ef migrations add AddEventsTable --project ../Data.SeaHavenIndustries
|
||||
dotnet ef database update
|
||||
|
||||
|
||||
STEP 3: SQL Script for Manual Table Creation (Alternative to migrations)
|
||||
-------------------------------------------------------
|
||||
*/
|
||||
|
||||
-- SQL SCRIPT TO CREATE EVENTS TABLE
|
||||
/*
|
||||
CREATE TABLE Events (
|
||||
Id INT PRIMARY KEY IDENTITY(1,1),
|
||||
Title NVARCHAR(100) NOT NULL,
|
||||
Description NVARCHAR(MAX) NULL,
|
||||
Location NVARCHAR(150) NULL,
|
||||
|
||||
RecordID INT NULL,
|
||||
RecordName NVARCHAR(255) NULL,
|
||||
RecordTargetType NVARCHAR(50) NULL,
|
||||
|
||||
ReminderMinutes INT NOT NULL DEFAULT 15,
|
||||
EventColor NVARCHAR(20) NOT NULL DEFAULT 'primary',
|
||||
|
||||
StartDate DATETIME NOT NULL,
|
||||
StartTime NVARCHAR(10) NULL,
|
||||
EndDate DATETIME NOT NULL,
|
||||
EndTime NVARCHAR(10) NULL,
|
||||
AllDay BIT NOT NULL DEFAULT 0,
|
||||
|
||||
-- Audit fields
|
||||
IsDeleted BIT NULL,
|
||||
createdby NVARCHAR(450) NULL,
|
||||
DeleterUserId NVARCHAR(450) NULL,
|
||||
DeletionTime DATETIME NULL,
|
||||
CreatedDate DATETIME NULL,
|
||||
LastModificationTime DATETIME NULL,
|
||||
LastModifierUserId INT NULL
|
||||
);
|
||||
|
||||
-- Insert sample data
|
||||
INSERT INTO Events (Title, Description, Location, StartDate, StartTime, EndDate, EndTime, AllDay, EventColor, ReminderMinutes, CreatedDate, IsDeleted)
|
||||
VALUES
|
||||
('Team Meeting', 'Weekly team sync', 'Conference Room A', '2026-04-01', '10:00 AM', '2026-04-01', '11:00 AM', 0, 'primary', 15, GETDATE(), 0),
|
||||
('Client Presentation', 'Q1 Results Review with ABC Corp', 'Virtual - Zoom', '2026-04-03', '02:00 PM', '2026-04-03', '03:30 PM', 0, 'success', 30, GETDATE(), 0),
|
||||
('Project Deadline', 'Final delivery for Project Phoenix', NULL, '2026-04-05', NULL, '2026-04-05', NULL, 1, 'danger', 1440, GETDATE(), 0),
|
||||
('Training Session', 'New system training for all employees', 'Main Office', '2026-04-08', '09:00 AM', '2026-04-08', '12:00 PM', 0, 'info', 60, GETDATE(), 0),
|
||||
('Equipment Maintenance', 'Scheduled maintenance for all assets', 'Warehouse', '2026-04-10', '08:00 AM', '2026-04-10', '05:00 PM', 0, 'warning', 120, GETDATE(), 0);
|
||||
*/
|
||||
|
||||
/*
|
||||
===================================================================================
|
||||
SECTION 5: IMPLEMENTATION CHECKLIST
|
||||
===================================================================================
|
||||
|
||||
BACKEND SETUP:
|
||||
--------------
|
||||
1. ✓ Create Events.cs in Data.SeaHavenIndustries/Models/
|
||||
2. ✓ Create Event_DTO.cs in Api.SeaHavenIndustries/DTOs/
|
||||
3. ✓ Create EditEvent_DTO.cs in Api.SeaHavenIndustries/DTOs/
|
||||
4. ✓ Create CalendarController.cs in Api.SeaHavenIndustries/Controllers/
|
||||
5. ☐ Add DbSet<Events> to ApplicationDbContext.cs
|
||||
6. ☐ Run migrations (or execute SQL script)
|
||||
7. ☐ Test all endpoints using Postman/Swagger
|
||||
|
||||
FRONTEND SETUP (Next Step):
|
||||
---------------------------
|
||||
1. ☐ Create src/pages/calendar/CalendarPage.js (List view with FullCalendar)
|
||||
2. ☐ Create src/pages/calendar/EventFormPage.js (Create/Edit form)
|
||||
3. ☐ Create src/pages/calendar/api.js (API calls)
|
||||
4. ☐ Add routes to App.js
|
||||
5. ☐ Install FullCalendar: npm install @fullcalendar/react @fullcalendar/daygrid @fullcalendar/timegrid @fullcalendar/interaction
|
||||
|
||||
API ENDPOINTS SUMMARY:
|
||||
---------------------
|
||||
POST /api/Calendar/AddEvent - Create new event
|
||||
POST /api/Calendar/EditEvent - Update existing event
|
||||
GET /api/Calendar/GetEventById/{id} - Get single event by ID
|
||||
DELETE /api/Calendar/DeleteEvent/{id} - Delete event (soft delete)
|
||||
POST /api/Calendar/GetEventList - Get paginated list with search
|
||||
GET /api/Calendar/GetEventsForCalendar - Get events for calendar view (with date range)
|
||||
|
||||
KEY FEATURES IMPLEMENTED:
|
||||
-------------------------
|
||||
✓ CRUD operations (Create, Read, Update, Delete)
|
||||
✓ Pagination with search filter
|
||||
✓ Date range filtering for calendar view
|
||||
✓ Soft delete (IsDeleted flag)
|
||||
✓ Related record linking (RecordID, RecordName, RecordTargetType)
|
||||
✓ Color coding for calendar display
|
||||
✓ All-day event support
|
||||
✓ Reminder settings
|
||||
✓ Proper error handling
|
||||
✓ Consistent with existing controller patterns (AssetController)
|
||||
✓ Fixed pagination bug (totalCount calculated AFTER filters)
|
||||
|
||||
NOTES:
|
||||
------
|
||||
- EventColor values: 'primary', 'success', 'info', 'warning', 'danger'
|
||||
- ReminderMinutes: 0=Never, 5, 10, 15, 30, 60, 120, 240, 360, 720, 1440 (1 day)
|
||||
- StartTime/EndTime format: "08:00 AM", "02:30 PM", etc.
|
||||
- AllDay: When true, time fields can be null/ignored
|
||||
- RecordTargetType: Can be "Account", "Contact", "WorkOrder", "FollowUp", etc.
|
||||
|
||||
===================================================================================
|
||||
END OF FILE
|
||||
===================================================================================
|
||||
*/
|
||||
|
|
@ -1,237 +0,0 @@
|
|||
# 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`
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
-- SQL Script to add missing columns to Locations table
|
||||
-- Run this in SQL Server Management Studio
|
||||
|
||||
USE [db_a7141e_seahavenindustry]
|
||||
GO
|
||||
|
||||
-- Check and add AccountId column
|
||||
IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Locations' AND COLUMN_NAME = 'AccountId')
|
||||
BEGIN
|
||||
ALTER TABLE [dbo].[Locations] ADD [AccountId] int NULL;
|
||||
PRINT 'Added AccountId column';
|
||||
END
|
||||
ELSE
|
||||
PRINT 'AccountId column already exists';
|
||||
|
||||
-- Check and add Address column
|
||||
IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Locations' AND COLUMN_NAME = 'Address')
|
||||
BEGIN
|
||||
ALTER TABLE [dbo].[Locations] ADD [Address] nvarchar(max) NULL;
|
||||
PRINT 'Added Address column';
|
||||
END
|
||||
ELSE
|
||||
PRINT 'Address column already exists';
|
||||
|
||||
-- Check and add City column
|
||||
IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Locations' AND COLUMN_NAME = 'City')
|
||||
BEGIN
|
||||
ALTER TABLE [dbo].[Locations] ADD [City] nvarchar(max) NULL;
|
||||
PRINT 'Added City column';
|
||||
END
|
||||
ELSE
|
||||
PRINT 'City column already exists';
|
||||
|
||||
-- Check and add Contact column
|
||||
IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Locations' AND COLUMN_NAME = 'Contact')
|
||||
BEGIN
|
||||
ALTER TABLE [dbo].[Locations] ADD [Contact] nvarchar(max) NULL;
|
||||
PRINT 'Added Contact column';
|
||||
END
|
||||
ELSE
|
||||
PRINT 'Contact column already exists';
|
||||
|
||||
-- Check and add ContactEmail column
|
||||
IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Locations' AND COLUMN_NAME = 'ContactEmail')
|
||||
BEGIN
|
||||
ALTER TABLE [dbo].[Locations] ADD [ContactEmail] nvarchar(max) NULL;
|
||||
PRINT 'Added ContactEmail column';
|
||||
END
|
||||
ELSE
|
||||
PRINT 'ContactEmail column already exists';
|
||||
|
||||
-- Check and add Phone column
|
||||
IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Locations' AND COLUMN_NAME = 'Phone')
|
||||
BEGIN
|
||||
ALTER TABLE [dbo].[Locations] ADD [Phone] nvarchar(max) NULL;
|
||||
PRINT 'Added Phone column';
|
||||
END
|
||||
ELSE
|
||||
PRINT 'Phone column already exists';
|
||||
|
||||
-- Check and add State column
|
||||
IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Locations' AND COLUMN_NAME = 'State')
|
||||
BEGIN
|
||||
ALTER TABLE [dbo].[Locations] ADD [State] nvarchar(max) NULL;
|
||||
PRINT 'Added State column';
|
||||
END
|
||||
ELSE
|
||||
PRINT 'State column already exists';
|
||||
|
||||
-- Check and add Status column
|
||||
IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Locations' AND COLUMN_NAME = 'Status')
|
||||
BEGIN
|
||||
ALTER TABLE [dbo].[Locations] ADD [Status] nvarchar(max) NULL;
|
||||
PRINT 'Added Status column';
|
||||
END
|
||||
ELSE
|
||||
PRINT 'Status column already exists';
|
||||
|
||||
-- Check and add ZipCode column
|
||||
IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Locations' AND COLUMN_NAME = 'ZipCode')
|
||||
BEGIN
|
||||
ALTER TABLE [dbo].[Locations] ADD [ZipCode] nvarchar(max) NULL;
|
||||
PRINT 'Added ZipCode column';
|
||||
END
|
||||
ELSE
|
||||
PRINT 'ZipCode column already exists';
|
||||
|
||||
-- Check and add foreign key index
|
||||
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Locations_AccountId' AND object_id = OBJECT_ID('dbo.Locations'))
|
||||
BEGIN
|
||||
CREATE NONCLUSTERED INDEX [IX_Locations_AccountId] ON [dbo].[Locations] ([AccountId] ASC);
|
||||
PRINT 'Added IX_Locations_AccountId index';
|
||||
END
|
||||
ELSE
|
||||
PRINT 'IX_Locations_AccountId index already exists';
|
||||
|
||||
-- Check and add foreign key constraint if Accounts table exists
|
||||
IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'Accounts')
|
||||
AND NOT EXISTS (SELECT * FROM sys.foreign_keys WHERE name = 'FK_Locations_Accounts_AccountId')
|
||||
BEGIN
|
||||
ALTER TABLE [dbo].[Locations] WITH CHECK ADD CONSTRAINT [FK_Locations_Accounts_AccountId]
|
||||
FOREIGN KEY([AccountId]) REFERENCES [dbo].[Accounts] ([Id]);
|
||||
PRINT 'Added FK_Locations_Accounts_AccountId foreign key';
|
||||
END
|
||||
ELSE
|
||||
PRINT 'FK_Locations_Accounts_AccountId foreign key already exists or Accounts table does not exist';
|
||||
|
||||
PRINT 'Schema fix completed!';
|
||||
GO
|
||||
|
|
@ -1,515 +0,0 @@
|
|||
# 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`
|
||||
Loading…
Add table
Reference in a new issue