diff --git a/ARCHITECTURE_AT_A_GLANCE.md b/ARCHITECTURE_AT_A_GLANCE.md deleted file mode 100644 index 635b2d5..0000000 --- a/ARCHITECTURE_AT_A_GLANCE.md +++ /dev/null @@ -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
• Authenticate users
• Map API DTOs
• Catch exceptions
• Return HTTP status codes | • Business logic
• Validation rules
• Database queries | -| **Service** | • Validate data (FluentValidation)
• Apply business rules
• Coordinate operations
• Transform data | • HTTP concerns
• Database operations | -| **DataService** | • Execute database queries
• CRUD operations
• Set audit fields | • Validation
• Business rules | -| **Data** | • Define entities
• Database schema | • Application logic | - ---- - -## 🔄 Request Flow - -### Creating a Contact (Example) - -``` -1. HTTP POST /api/Contact/Create - ↓ -2. ContactController.Create() - - Checks authentication - - Gets userId from claims - ↓ -3. ContactService.CreateContactAsync() - - Validates using FluentValidation - - Applies business rules - ↓ -4. ContactDataService.CreateAsync() - - Creates entity - - Sets audit fields - - Saves to database - ↓ -5. Returns through layers: - Entity → DataDTO → ServiceDTO → ApiDTO → HTTP Response -``` - ---- - -## 📦 DTO Flow - -``` -Frontend (JSON) - ↓ -API DTO (Contact_DTO) ← Controller receives this - ↓ -Service DTO (CreateContactDTO) ← Service validates this - ↓ -Data DTO (CreateContactDataDTO) ← DataService uses this - ↓ -Entity (Contact) ← Database stores this -``` - ---- - -## 🔌 Dependency Injection - -### Automatic Assembly Scanning - -Services auto-register via assembly scanning (no manual registration needed). - -```csharp -// Program.cs -builder.Services.AddDataServices(); // Auto-register data services -builder.Services.AddBusinessServices(); // Auto-register business services + validators -``` - -### Naming Convention (REQUIRED) - -| Interface | Implementation | Registered? | -|-----------|---------------|-------------| -| `IContactService` | `ContactService` | ✅ YES | -| `ISomething` | `SomethingElse` | ❌ NO | - -**Rule**: Interface = `I` + Class name exactly - ---- - -## ✅ Validation - -### Where and How - -``` -❌ Controller → Catches ValidationException -✅ Service → Runs FluentValidation.Validate() -❌ DataService → No validation -``` - -### FluentValidation Example - -```csharp -public class CreateContactDTOValidator : AbstractValidator -{ - public CreateContactDTOValidator() - { - RuleFor(x => x.FirstName) - .NotEmpty().WithMessage("First name is required") - .MaximumLength(50); - - RuleFor(x => x.Email) - .EmailAddress().WithMessage("Invalid email"); - } -} -``` - -### In Service - -```csharp -var validationResult = await _validator.ValidateAsync(dto); -if (!validationResult.IsValid) -{ - throw new ValidationException(validationResult.Errors); -} -``` - ---- - -## 🚨 Exception Handling - -### Standard Pattern (All Controllers) - -```csharp -try -{ - var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); - if (userId == null) - return Unauthorized(...); - - await _service.SomeMethod(dto, userId); - return Ok(...); -} -catch (ValidationException vex) // 400 Bad Request -{ - return BadRequest(...); -} -catch (KeyNotFoundException) // 404 Not Found -{ - return NotFound(...); -} -catch (Exception ex) // 500 Internal Server Error -{ - return StatusCode(500, ...); -} -``` - ---- - -## 🔐 Authentication - -### Getting User ID - -```csharp -// Method 1: Claims (Preferred) -var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); - -// Method 2: UserManager (for Identity) -var userId = _userManager.GetUserId(User); - -// Always check for null -if (userId == null) - return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" }); -``` - -### Audit Fields - -Every entity has: -```csharp -public string CreatedBy { get; set; } // User ID (GUID) -public DateTime CreatedDate { get; set; } // UTC timestamp -public string ModifiedBy { get; set; } // User ID (GUID) -public DateTime ModifiedDate { get; set; } // UTC timestamp -``` - -Set in DataService: -```csharp -CreatedBy = userId, -CreatedDate = DateTime.UtcNow, -ModifiedBy = userId, -ModifiedDate = DateTime.UtcNow -``` - ---- - -## 📋 Naming Conventions - -| Component | Pattern | Example | -|-----------|---------|---------| -| Entity | `[Entity]` | `Contact` | -| Controller | `[Entity]Controller` | `ContactController` | -| Service Interface | `I[Entity]Service` | `IContactService` | -| Service Class | `[Entity]Service` | `ContactService` | -| DataService Interface | `I[Entity]DataService` | `IContactDataService` | -| DataService Class | `[Entity]DataService` | `ContactDataService` | -| API DTO | `[Entity]_DTO` | `Contact_DTO` | -| Service DTO | `[Action][Entity]DTO` | `CreateContactDTO` | -| Data DTO | `[Action][Entity]DataDTO` | `CreateContactDataDTO` | -| Validator | `[DTO]Validator` | `CreateContactDTOValidator` | - ---- - -## 🎨 Code Templates - -### Controller Method Template -```csharp -[HttpPost("ActionName")] -public async Task ActionName([FromBody] SomeDTO dto) -{ - try - { - var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); - if (userId == null) - return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" }); - - await _service.Method(dto, userId); - - return Ok(new Response { Status = "Success", Message = "Success message" }); - } - catch (ValidationException vex) - { - var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage)); - return BadRequest(new Response { Status = "Validation Error", Message = errors }); - } - catch (KeyNotFoundException) - { - return NotFound(new Response { Status = "Error", Message = "Not found" }); - } - catch (Exception ex) - { - return StatusCode(500, new Response { Status = "Error", Message = ex.Message }); - } -} -``` - -### Service Method Template -```csharp -public async Task MethodAsync(InputDTO dto, string userId) -{ - // 1. Validate - var validationResult = await _validator.ValidateAsync(dto); - if (!validationResult.IsValid) - { - throw new ValidationException(validationResult.Errors); - } - - // 2. Business logic (if any) - - // 3. Call DataService - var result = await _dataService.MethodAsync(dataDto, userId); - - // 4. Map and return - return MapToServiceDTO(result); -} -``` - -### DataService Method Template -```csharp -public async Task MethodAsync(InputDataDTO dto, string userId) -{ - // 1. Create/modify entity - var entity = new Entity - { - Property = dto.Property, - CreatedBy = userId, - CreatedDate = DateTime.UtcNow, - ModifiedBy = userId, - ModifiedDate = DateTime.UtcNow - }; - - // 2. Database operation - _db.Entities.Add(entity); - await _db.SaveChangesAsync(); - - // 3. Map and return - return MapToDataDTO(entity); -} -``` - ---- - -## 📚 Key Rules - -1. **Never skip layers** - Always go Controller → Service → DataService -2. **No direct `_db` in Controllers** - Database access only in DataService -3. **Validate in Service** - Use FluentValidation, not manual checks -4. **Always check userId** - Return 401 if null -5. **Catch ValidationException first** - Before generic Exception -6. **Use interfaces everywhere** - For dependency injection -7. **Set audit fields** - CreatedBy, CreatedDate, ModifiedBy, ModifiedDate -8. **Return proper HTTP codes** - 200, 400, 401, 404, 500 - ---- - -## 🎓 Learn More - -- **Full Details**: See `BACKEND_ARCHITECTURE.md` -- **Step-by-Step Guide**: See `QUICK_START_GUIDE.md` -- **Working Examples**: Look at `ContactController`, `ContactService`, `ContactDataService` diff --git a/CALENDAR_MODULE_COMPLETE.cs b/CALENDAR_MODULE_COMPLETE.cs deleted file mode 100644 index 3e3bd12..0000000 --- a/CALENDAR_MODULE_COMPLETE.cs +++ /dev/null @@ -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 _userManager; - private readonly ApplicationDbContext _db; - - public CalendarController(UserManager userManager, ApplicationDbContext db) - { - _userManager = userManager; - _db = db; - } - - // =================================================================================== - // CREATE EVENT - // =================================================================================== - [HttpPost] - [Route("AddEvent")] - public async Task 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 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 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 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 { 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 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 -=================================================================================== -*/ diff --git a/DI_ASSEMBLY_SCANNING_GUIDE.md b/DI_ASSEMBLY_SCANNING_GUIDE.md deleted file mode 100644 index b678c9c..0000000 --- a/DI_ASSEMBLY_SCANNING_GUIDE.md +++ /dev/null @@ -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(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -// ... hundreds of lines -``` - ---- - -## 📦 DataServicesModule - -**Location**: `SeaHaven.DataServices/DependencyInjection/DataServicesModule.cs` - -```csharp -public static IServiceCollection AddDataServices(this IServiceCollection services) -{ - var assembly = Assembly.GetExecutingAssembly(); - - // Find all non-abstract classes in the assembly - var allClasses = assembly.GetTypes() - .Where(t => t.IsClass && !t.IsAbstract && !t.IsGenericType) - .ToList(); - - foreach (var implementationType in allClasses) - { - // Find interface matching pattern: I{ClassName} - var defaultInterface = implementationType.GetInterfaces() - .FirstOrDefault(i => i.Name == $"I{implementationType.Name}"); - - if (defaultInterface != null) - { - // Register as Scoped - services.AddScoped(defaultInterface, implementationType); - } - } - - return services; -} -``` - -### What It Does - -1. Scans entire `SeaHaven.DataServices` assembly -2. Finds all concrete classes (e.g., `ContactDataService`, `EmployeeDataService`) -3. For each class, looks for an interface named `I{ClassName}` -4. If found, registers the pair as **Scoped** service - -### Example - -If you create: -- `IProductDataService` (interface) -- `ProductDataService` (class implementing `IProductDataService`) - -Then `IProductDataService → ProductDataService` is **automatically registered** as Scoped. - ---- - -## 💼 ServicesModule - -**Location**: `SeaHaven.Services/DependencyInjection/ServicesModule.cs` - -```csharp -public static IServiceCollection AddBusinessServices(this IServiceCollection services) -{ - var assembly = Assembly.GetExecutingAssembly(); - - var allClasses = assembly.GetTypes() - .Where(t => t.IsClass && !t.IsAbstract && !t.IsGenericType) - .ToList(); - - foreach (var implementationType in allClasses) - { - var defaultInterface = implementationType.GetInterfaces() - .FirstOrDefault(i => i.Name == $"I{implementationType.Name}"); - - if (defaultInterface != null) - { - services.AddScoped(defaultInterface, implementationType); - } - } - - // ALSO auto-register all FluentValidation validators - services.AddValidatorsFromAssembly(assembly); - - return services; -} -``` - -### What It Does - -1. Scans entire `SeaHaven.Services` assembly -2. Registers all services using naming convention (same as DataServices) -3. **BONUS**: Automatically registers ALL FluentValidation validators in the assembly - -### Example - -If you create: -- `IProductService` (interface) -- `ProductService` (class) -- `CreateProductDTOValidator` (FluentValidation validator) - -Then **all three** are automatically registered. - ---- - -## ⚠️ CRITICAL: Naming Convention - -For auto-registration to work, you MUST follow this pattern: - -### ✅ CORRECT - -| Interface | Implementation | Result | -|-----------|---------------|--------| -| `IContactService` | `ContactService` | ✅ Registered as Scoped | -| `IContactDataService` | `ContactDataService` | ✅ Registered as Scoped | -| `IProductService` | `ProductService` | ✅ Registered as Scoped | -| `IEmployeeService` | `EmployeeService` | ✅ Registered as Scoped | - -### ❌ WRONG - -| Interface | Implementation | Result | -|-----------|---------------|--------| -| `IProductService` | `ProductServiceImpl` | ❌ NOT registered - names don't match | -| `IContactRepository` | `ContactService` | ❌ NOT registered - names don't match | -| `ISomething` | `SomethingElse` | ❌ NOT registered - names don't match | - ---- - -## 📋 Rules - -1. **Interface naming**: `I{ClassName}` -2. **Class naming**: `{ClassName}` -3. **Example**: - - Interface: `IContactService` - - Class: `ContactService` (NOT `ContactServiceImpl`, `ContactServiceImplementation`, etc.) - ---- - -## 🚀 Benefits - -✅ **Less boilerplate** - No need to add each service to `Program.cs` -✅ **Auto-discovery** - New services are automatically registered -✅ **Consistent** - All services registered the same way -✅ **FluentValidation included** - Validators work automatically -✅ **Cleaner Program.cs** - Only 2 lines instead of 50+ - ---- - -## 🎯 How to Add a New Service - -### Old Way (Manual) -```csharp -// 1. Create interface and class -public interface IProductService { } -public class ProductService : IProductService { } - -// 2. Manually add to Program.cs -builder.Services.AddScoped(); // ❌ DON'T DO THIS -``` - -### New Way (Automatic) -```csharp -// 1. Create interface and class following naming convention -public interface IProductService { } -public class ProductService : IProductService { } - -// 2. Done! It's automatically registered ✅ -``` - ---- - -## 🔧 Lifecycle - -All services are registered as **Scoped**: -- New instance per HTTP request -- Shared within the same request -- Disposed at end of request - ---- - -## 📚 Example Flow - -1. You create `IProductDataService` and `ProductDataService` in `SeaHaven.DataServices` -2. You create `IProductService` and `ProductService` in `SeaHaven.Services` -3. You create `CreateProductDTOValidator` in `SeaHaven.Services/Validation` -4. Application starts → `AddDataServices()` and `AddBusinessServices()` run -5. Assembly scanner finds your services and validator -6. All registered automatically as Scoped -7. You inject `IProductService` in controller → works immediately ✅ - ---- - -## 🐛 Troubleshooting - -### Service not being injected? - -Check: -1. ✅ Is the interface named `I{ClassName}`? -2. ✅ Is the class named `{ClassName}`? -3. ✅ Is the class in the correct assembly? -4. ✅ Is the class public and concrete (not abstract)? -5. ✅ Does the class implement the interface? - -### Validator not working? - -Check: -1. ✅ Is the validator in `SeaHaven.Services` assembly? -2. ✅ Does it inherit from `AbstractValidator`? -3. ✅ Is it public and concrete? - ---- - -## 📖 See Also - -- **Full Architecture Guide**: `BACKEND_ARCHITECTURE.md` -- **Quick Start Guide**: `QUICK_START_GUIDE.md` -- **At a Glance**: `ARCHITECTURE_AT_A_GLANCE.md` diff --git a/FIX_LOCATIONS_SCHEMA.sql b/FIX_LOCATIONS_SCHEMA.sql deleted file mode 100644 index 042c7a8..0000000 --- a/FIX_LOCATIONS_SCHEMA.sql +++ /dev/null @@ -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 diff --git a/QUICK_START_GUIDE.md b/QUICK_START_GUIDE.md deleted file mode 100644 index c116639..0000000 --- a/QUICK_START_GUIDE.md +++ /dev/null @@ -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 Products { get; set; } -``` - ---- - -## Step 2: Create Migration - -```bash -cd backend/shoc-backend -dotnet ef migrations add AddProductTable --project Data.SeaHavenIndustries --startup-project Api.SeaHavenIndustries -dotnet ef database update --project Data.SeaHavenIndustries --startup-project Api.SeaHavenIndustries -``` - ---- - -## Step 3: Create DTOs - -### 3A. Data Layer DTOs (`SeaHaven.DataServices/DTOs/`) - -**CreateProductDataDTO.cs** -```csharp -public class CreateProductDataDTO -{ - public string Name { get; set; } = string.Empty; - public string? Description { get; set; } - public decimal Price { get; set; } -} -``` - -**ProductDataDTO.cs** -```csharp -public class ProductDataDTO -{ - public int Id { get; set; } - public string Name { get; set; } = string.Empty; - public string? Description { get; set; } - public decimal Price { get; set; } - public bool IsActive { get; set; } -} -``` - -### 3B. Service Layer DTOs (`SeaHaven.Services/DTOs/`) - -**CreateProductDTO.cs** -```csharp -public class CreateProductDTO -{ - public string Name { get; set; } = string.Empty; - public string? Description { get; set; } - public decimal Price { get; set; } -} -``` - -**ProductDTO.cs** -```csharp -public class ProductDTO -{ - public int Id { get; set; } - public string Name { get; set; } = string.Empty; - public string? Description { get; set; } - public decimal Price { get; set; } - public bool IsActive { get; set; } -} -``` - -### 3C. API Layer DTOs (`Api.SeaHavenIndustries/DTOs/`) - -**Product_DTO.cs** -```csharp -public class Product_DTO -{ - public string Name { get; set; } = string.Empty; - public string? Description { get; set; } - public decimal Price { get; set; } -} -``` - ---- - -## Step 4: Create FluentValidation Validator - -**File**: `SeaHaven.Services/Validators/CreateProductDTOValidator.cs` - -```csharp -using FluentValidation; -using SeaHaven.Services.DTOs; - -public class CreateProductDTOValidator : AbstractValidator -{ - public CreateProductDTOValidator() - { - RuleFor(x => x.Name) - .NotEmpty().WithMessage("Product name is required") - .MaximumLength(100).WithMessage("Name cannot exceed 100 characters"); - - RuleFor(x => x.Price) - .GreaterThan(0).WithMessage("Price must be greater than 0"); - } -} -``` - ---- - -## Step 5: Create DataService - -### 5A. Interface (`SeaHaven.DataServices/Interfaces/IProductDataService.cs`) - -```csharp -public interface IProductDataService -{ - Task CreateAsync(CreateProductDataDTO dto, string userId); - Task UpdateAsync(int id, UpdateProductDataDTO dto, string userId); - Task DeleteAsync(int id, string userId); - Task GetByIdAsync(int id); - Task> GetPagedAsync(int page, int pageSize, string? search); -} -``` - -### 5B. Implementation (`SeaHaven.DataServices/Implementation/ProductDataService.cs`) - -```csharp -using Data.SeaHavenIndustries; -using Data.SeaHavenIndustries.Models; -using Microsoft.EntityFrameworkCore; - -public class ProductDataService : IProductDataService -{ - private readonly ApplicationDbContext _db; - - public ProductDataService(ApplicationDbContext db) - { - _db = db; - } - - public async Task CreateAsync(CreateProductDataDTO dto, string userId) - { - var product = new Product - { - Name = dto.Name, - Description = dto.Description, - Price = dto.Price, - IsActive = true, - CreatedBy = userId, - CreatedDate = DateTime.UtcNow, - ModifiedBy = userId, - ModifiedDate = DateTime.UtcNow - }; - - _db.Products.Add(product); - await _db.SaveChangesAsync(); - - return new ProductDataDTO - { - Id = product.Id, - Name = product.Name, - Description = product.Description, - Price = product.Price, - IsActive = product.IsActive - }; - } - - public async Task GetByIdAsync(int id) - { - var product = await _db.Products.FindAsync(id); - if (product == null) return null; - - return new ProductDataDTO - { - Id = product.Id, - Name = product.Name, - Description = product.Description, - Price = product.Price, - IsActive = product.IsActive - }; - } - - // Implement other methods... -} -``` - ---- - -## Step 6: Create Service - -### 6A. Interface (`SeaHaven.Services/Interfaces/IProductService.cs`) - -```csharp -public interface IProductService -{ - Task CreateProductAsync(CreateProductDTO dto, string userId); - Task UpdateProductAsync(int id, UpdateProductDTO dto, string userId); - Task DeleteProductAsync(int id, string userId); - Task GetProductByIdAsync(int id); - Task> GetProductsPagedAsync(int page, int pageSize, string? search); -} -``` - -### 6B. Implementation (`SeaHaven.Services/Implementation/ProductService.cs`) - -```csharp -using FluentValidation; -using SeaHaven.DataServices.Interfaces; -using SeaHaven.Services.DTOs; -using SeaHaven.Services.Interfaces; - -public class ProductService : IProductService -{ - private readonly IProductDataService _dataService; - private readonly IValidator _createValidator; - - public ProductService( - IProductDataService dataService, - IValidator createValidator) - { - _dataService = dataService; - _createValidator = createValidator; - } - - public async Task CreateProductAsync(CreateProductDTO dto, string userId) - { - // Validate - var validationResult = await _createValidator.ValidateAsync(dto); - if (!validationResult.IsValid) - { - throw new ValidationException(validationResult.Errors); - } - - // Map to Data DTO - var dataDto = new CreateProductDataDTO - { - Name = dto.Name, - Description = dto.Description, - Price = dto.Price - }; - - // Call DataService - var created = await _dataService.CreateAsync(dataDto, userId); - - // Map to Service DTO - return new ProductDTO - { - Id = created.Id, - Name = created.Name, - Description = created.Description, - Price = created.Price, - IsActive = created.IsActive - }; - } - - public async Task GetProductByIdAsync(int id) - { - var product = await _dataService.GetByIdAsync(id); - if (product == null) - throw new KeyNotFoundException($"Product with ID {id} not found"); - - return new ProductDTO - { - Id = product.Id, - Name = product.Name, - Description = product.Description, - Price = product.Price, - IsActive = product.IsActive - }; - } - - // Implement other methods... -} -``` - ---- - -## Step 7: Create Controller - -**File**: `Api.SeaHavenIndustries/Controllers/ProductController.cs` - -```csharp -using Api.SeaHavenIndustries.DTOs; -using FluentValidation; -using Microsoft.AspNetCore.Mvc; -using SeaHaven.Services.Interfaces; -using System.Security.Claims; - -[ApiController] -[Route("api/[controller]")] -public class ProductController : Controller -{ - private readonly IProductService _productService; - - public ProductController(IProductService productService) - { - _productService = productService; - } - - [HttpPost("Create")] - public async Task Create([FromBody] Product_DTO dto) - { - try - { - var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); - if (userId == null) - return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" }); - - var serviceDto = new CreateProductDTO - { - Name = dto.Name, - Description = dto.Description, - Price = dto.Price - }; - - await _productService.CreateProductAsync(serviceDto, userId); - - return Ok(new Response { Status = "Success", Message = "Product created successfully" }); - } - catch (ValidationException vex) - { - var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage)); - return BadRequest(new Response { Status = "Validation Error", Message = errors }); - } - catch (Exception ex) - { - return StatusCode(500, new Response { Status = "Error", Message = ex.Message }); - } - } - - [HttpGet("GetById")] - public async Task GetById(int id) - { - try - { - var product = await _productService.GetProductByIdAsync(id); - return Ok(product); - } - catch (KeyNotFoundException) - { - return NotFound(new Response { Status = "Error", Message = "Product not found" }); - } - catch (Exception ex) - { - return StatusCode(500, new Response { Status = "Error", Message = ex.Message }); - } - } - - // Add other endpoints... -} -``` - ---- - -## Step 8: Services Auto-Registered ✅ - -**No action needed!** Services are automatically registered by assembly scanning. - -If you followed naming conventions (`IProductService` → `ProductService`), your services are already registered. - -**How?** These lines in `Program.cs` scan assemblies: -```csharp -builder.Services.AddDataServices(); // Scans SeaHaven.DataServices -builder.Services.AddBusinessServices(); // Scans SeaHaven.Services + Validators -``` - ---- - -## Step 9: Test - -```bash -# Build -dotnet build - -# Run -dotnet run --project Api.SeaHavenIndustries - -# Test endpoint -curl -X POST "http://localhost:5141/api/Product/Create" \ - -H "Content-Type: application/json" \ - -d '{"name":"Test Product","description":"Test","price":99.99}' -``` - ---- - -## ✅ Checklist - -- [ ] Entity created with audit fields -- [ ] DbSet added to DbContext -- [ ] Migration created and applied -- [ ] Data DTOs created -- [ ] Service DTOs created -- [ ] API DTOs created -- [ ] FluentValidation validator created -- [ ] DataService interface created -- [ ] DataService implementation created -- [ ] Service interface created -- [ ] Service implementation created -- [ ] Controller created -- [ ] Services registered in DI container -- [ ] Exception handling implemented -- [ ] User ID authentication added -- [ ] Build succeeds -- [ ] Endpoints tested - ---- - -## 🎯 Common Patterns - -### Get List with Pagination -```csharp -[HttpGet("GetList")] -public async Task GetList(string? search = "", int page = 1, int pageSize = 10) -{ - var result = await _productService.GetProductsPagedAsync(page, pageSize, search); - return Ok(new Pagination_DTO - { - Data = result.Items, - PageNumber = page, - PageSize = pageSize, - TotalCount = result.TotalCount, - TotalPages = (int)Math.Ceiling(result.TotalCount / (double)pageSize) - }); -} -``` - -### Update -```csharp -[HttpPost("Update")] -public async Task Update([FromBody] EditProduct_DTO dto) -{ - try - { - var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); - if (userId == null) - return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" }); - - await _productService.UpdateProductAsync(dto.Id, dto.ToServiceDTO(), userId); - return Ok(new Response { Status = "Success", Message = "Updated" }); - } - catch (ValidationException vex) - { - var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage)); - return BadRequest(new Response { Status = "Validation Error", Message = errors }); - } - catch (KeyNotFoundException) - { - return NotFound(new Response { Status = "Error", Message = "Not found" }); - } - catch (Exception ex) - { - return StatusCode(500, new Response { Status = "Error", Message = ex.Message }); - } -} -``` - -### Delete -```csharp -[HttpPost("Delete")] -public async Task Delete(int id) -{ - try - { - var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); - if (userId == null) - return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" }); - - await _productService.DeleteProductAsync(id, userId); - return Ok(new Response { Status = "Success", Message = "Deleted" }); - } - catch (KeyNotFoundException) - { - return NotFound(new Response { Status = "Error", Message = "Not found" }); - } - catch (Exception ex) - { - return StatusCode(500, new Response { Status = "Error", Message = ex.Message }); - } -} -``` - ---- - -## 📚 Reference Implementations - -For complete working examples, see: -- `ContactController`, `ContactService`, `ContactDataService` -- `EmployeeController`, `EmployeeService`, `EmployeeDataService` -- `AssetController`, `AssetService`, `AssetDataService`