fixing architecture

This commit is contained in:
npalOmega 2026-05-14 11:09:17 -05:00
parent 59385cf5b1
commit 57d378125f
5 changed files with 0 additions and 1758 deletions

View file

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

View file

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

View file

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

View file

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

View file

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