using Api.SeaHavenIndustries.DTOs; using Api.SeaHavenIndustries.Helper; using Data.SeaHavenIndustries; using FluentValidation; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using SeaHaven.Services.Interfaces; using SeaHaven.Services.DTOs; using System.Security.Claims; namespace Api.SeaHavenIndustries.Controllers { [Authorize] [ApiController] [Route("api/[controller]")] [Route("api/vendors")] public class VendorController : Controller { private readonly IVendorService _vendorService; private readonly ILogger _logger; public VendorController(IVendorService vendorService, ILogger logger) { _vendorService = vendorService; _logger = logger; } // SH-198 legacy per-technician directory contract. Existing consumers depend // on one row per technician (no grouped Technicians) and on raw pagination // semantics, including pageSize values above the grouped-directory bound. [HttpGet("GetVendorList")] public async Task GetVendorList( string? search = "", int page = 1, int pageSize = 10, bool? isActive = true, [FromQuery] string[]? companies = null, [FromQuery] string[]? trades = null, [FromQuery] string[]? locations = null, [FromQuery] string[]? jobBuckets = null, CancellationToken cancellationToken = default) { var pagedResult = await _vendorService.GetVendorTechnicianDirectoryPagedAsync( page, pageSize, search, isActive, companies, trades, locations, jobBuckets, cancellationToken); var data = pagedResult.Items.Select(v => new { v.Id, CompanyName = v.CompanyName, v.CompanyId, v.ContactName, v.Email, v.Phone, v.CompanyPhone, v.PreferredContact, v.Address, v.City, v.State, Zip = v.Zipcode, v.TradeSpecialties, v.GoogleMapsUrl, v.Notes, v.IsActive, v.TotalJobs }); return Ok(new Pagination_DTO { Data = data, PageNumber = page, PageSize = pageSize, TotalCount = pagedResult.TotalCount, TotalPages = (int)Math.Ceiling(pagedResult.TotalCount / (double)pageSize) }); } // SH-281 company-grouped directory: one row per company with the full // status-eligible technician roster nested under Technicians and bounded // page sizes. Top-level Email is the company email; technician emails stay // on the nested rows. [HttpGet("GetVendorDirectoryList")] public async Task GetVendorDirectoryList( string? search = "", int page = 1, int pageSize = 10, bool? isActive = true, [FromQuery] string[]? companies = null, [FromQuery] string[]? trades = null, [FromQuery] string[]? locations = null, [FromQuery] string[]? jobBuckets = null, [FromQuery] string[]? areas = null, CancellationToken cancellationToken = default) { var pagedResult = await _vendorService.GetVendorCompanyDirectoryPagedAsync( page, pageSize, search, isActive, companies, trades, locations, jobBuckets, areas, cancellationToken); var data = pagedResult.Items.Select(v => new { v.Id, CompanyName = v.CompanyName, v.CompanyId, v.ContactName, v.Email, v.Phone, v.CompanyPhone, v.PreferredContact, v.Address, v.City, v.State, Zip = v.Zipcode, v.TradeSpecialties, v.GoogleMapsUrl, v.Notes, v.IsActive, v.TotalJobs, v.AreaId, v.AreaName, Technicians = v.Technicians.Select(t => new { t.Id, t.ContactName, t.Email, t.Phone, t.PreferredContact, t.TradeSpecialties, t.IsActive, t.TotalJobs }) }); return Ok(new Pagination_DTO { Data = data, PageNumber = pagedResult.Page, PageSize = pagedResult.PageSize, TotalCount = pagedResult.TotalCount, TotalPages = (int)Math.Ceiling(pagedResult.TotalCount / (double)pagedResult.PageSize) }); } [HttpGet("{id}")] [HttpGet("GetById")] public async Task GetVendorById([FromRoute] int? id, [FromQuery(Name = "id")] int? queryId) { var vendorId = id ?? queryId; if (vendorId == null) return BadRequest(new Response { Status = "Error", Message = "Id is required" }); var vendor = await _vendorService.GetVendorByIdAsync(vendorId.Value); if (vendor == null) return NotFound(new Response { Status = "Error", Message = "Vendor not found" }); return Ok(new { vendor.Id, CompanyName = vendor.Name, vendor.CompanyId, vendor.ContactName, vendor.Email, vendor.Phone, vendor.CompanyPhone, vendor.PreferredContact, vendor.Address, vendor.City, vendor.State, Zip = vendor.Zipcode, vendor.TradeSpecialties, vendor.GoogleMapsUrl, vendor.Notes, vendor.IsActive, vendor.TotalJobs }); } [HttpPost] [HttpPost("Create")] public async Task Create([FromBody] Vendor_DTO model) { try { var createDto = new CreateVendorDTO { Name = model.CompanyName ?? throw new ArgumentException("CompanyName is required"), CompanyId = model.CompanyId, ContactName = model.ContactName, Email = model.Email, Phone = model.Phone, CompanyPhone = model.CompanyPhone, PreferredContact = model.PreferredContact, Address = model.Address, City = model.City, State = model.State, Zipcode = model.Zip, TradeSpecialties = model.TradeSpecialties, GoogleMapsUrl = model.GoogleMapsUrl, Notes = model.Notes, IsActive = model.IsActive ?? true }; var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); if (userId == null) return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" }); await _vendorService.CreateVendorAsync(createDto, userId); return Ok(new DataResponse { Message = "Vendor Created", Status = "200" }); } catch (ValidationException vex) { var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage)); return BadRequest(new Response { Status = "Validation Error", Message = errors }); } catch (Exception ex) { return StatusCode(500, new Response { Status = "Error", Message = _logger.Sanitize(ex) }); } } [HttpPut("{id}")] [HttpPost("Update")] public async Task Update([FromRoute] int? id, [FromBody] EditVendor_DTO model) { try { // For named route, id comes from model int vendorId = id ?? model.Id; if (vendorId == 0) return BadRequest(new Response { Status = "Error", Message = "Vendor Id is required" }); var updateDto = new UpdateVendorDTO { Name = model.CompanyName, CompanyId = model.CompanyId, ContactName = model.ContactName, Email = model.Email, Phone = model.Phone, CompanyPhone = model.CompanyPhone, PreferredContact = model.PreferredContact, Address = model.Address, City = model.City, State = model.State, Zipcode = model.Zip, TradeSpecialties = model.TradeSpecialties, GoogleMapsUrl = model.GoogleMapsUrl, Notes = model.Notes, IsActive = model.IsActive, ConfirmOpenWorkOrders = model.ConfirmOpenWorkOrders }; var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); if (userId == null) return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" }); await _vendorService.UpdateVendorAsync(vendorId, updateDto, userId); return Ok(new DataResponse { Message = "Vendor Updated", Status = "200" }); } catch (ValidationException vex) { var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage)); return BadRequest(new Response { Status = "Validation Error", Message = errors }); } catch (VendorDeactivationBlockedException dbex) { return Conflict(new { Status = "Conflict", Message = _logger.Sanitize(dbex, "Vendor cannot be deactivated because it has open work orders"), OpenWorkOrders = dbex.OpenWorkOrders }); } catch (InvalidOperationException) { return NotFound(new Response { Status = "Error", Message = "Vendor not found" }); } catch (Exception ex) { return StatusCode(500, new Response { Status = "Error", Message = _logger.Sanitize(ex) }); } } [HttpDelete("{id}")] [HttpPost("Delete")] public async Task Delete( [FromRoute] int? id, [FromQuery(Name = "id")] int? queryId = null, [FromQuery] bool confirmOpenWorkOrders = false) { try { // Support both route parameter and query string int vendorId = id ?? queryId ?? 0; if (vendorId == 0) return BadRequest(new Response { Status = "Error", Message = "Vendor Id is required" }); var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); if (userId == null) return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" }); await _vendorService.DeleteVendorAsync(vendorId, userId, confirmOpenWorkOrders); return Ok(new DataResponse { Message = "Vendor Deactivated", Status = "200" }); } catch (VendorDeactivationBlockedException dbex) { return Conflict(new { Status = "Conflict", Message = _logger.Sanitize(dbex, "Vendor cannot be deactivated because it has open work orders"), OpenWorkOrders = dbex.OpenWorkOrders }); } catch (InvalidOperationException) { return NotFound(new Response { Status = "Error", Message = "Vendor not found" }); } catch (Exception ex) { return StatusCode(500, new Response { Status = "Error", Message = _logger.Sanitize(ex) }); } } [HttpGet("{id:int}/deactivation-impact")] public async Task GetDeactivationImpact(int id) { try { var impact = await _vendorService.GetDeactivationImpactAsync(id); return Ok(impact); } catch (InvalidOperationException) { return NotFound(new Response { Status = "Error", Message = "Vendor not found" }); } catch (Exception ex) { return StatusCode(500, new Response { Status = "Error", Message = _logger.Sanitize(ex) }); } } [HttpPut("{id:int}/work-order-update")] public async Task UpdateFromWorkOrder(int id, [FromBody] WorkOrderVendorUpdate_DTO model) { try { if (model == null || model.WorkOrderId <= 0) return BadRequest(new Response { Status = "Error", Message = "A valid workOrderId is required" }); var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); if (userId == null) return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" }); var dto = new WorkOrderVendorUpdateDTO { WorkOrderId = model.WorkOrderId, ContactName = model.ContactName, PreferredContact = model.PreferredContact, Phone = model.Phone, Email = model.Email, Notes = model.Notes }; var result = await _vendorService.UpdateVendorFromWorkOrderAsync(id, dto, userId); return Ok(result); } catch (ValidationException vex) { var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage)); return BadRequest(new Response { Status = "Validation Error", Message = errors }); } catch (InvalidOperationException) { return NotFound(new Response { Status = "Error", Message = "Vendor or work order link not found" }); } catch (Exception ex) { return StatusCode(500, new Response { Status = "Error", Message = _logger.Sanitize(ex) }); } } [HttpGet("Dropdown")] public async Task GetDropdown([FromQuery] string? trade = null, [FromQuery] string? siteZip = null, CancellationToken cancellationToken = default) { var result = await _vendorService.GetDropdownAsync(trade, siteZip, cancellationToken); return Ok(result); } [HttpGet("facets")] public async Task GetFacets([FromQuery] bool? isActive = null, CancellationToken cancellationToken = default) { var result = await _vendorService.GetFacetsAsync(isActive, cancellationToken); return Ok(result); } [HttpGet("{id:int}/portal-token")] public async Task GetPortalToken(int id, CancellationToken cancellationToken = default) { var result = await _vendorService.GetPortalTokenAsync(id, cancellationToken); if (result == null) return NotFound(); return Ok(result); } [HttpPost("{id:int}/portal-token/rotate")] public async Task RotatePortalToken(int id, CancellationToken cancellationToken = default) { var result = await _vendorService.RotatePortalTokenAsync(id, cancellationToken); if (result == null) return NotFound(); return Ok(result); } [HttpPost("{id:int}/portal-token/revoke")] public async Task RevokePortalToken(int id, CancellationToken cancellationToken = default) { var success = await _vendorService.RevokePortalTokenAsync(id, cancellationToken); if (!success) return NotFound(); return Ok(new { revoked = true }); } } }