Compare commits

...

49 commits

Author SHA1 Message Date
npalOmega
5e4d9894e9 backend architectural template 2026-05-06 10:49:33 -05:00
npalOmega
ac76b201de refactor 2026-04-28 18:55:14 -05:00
npalOmega
64adcae4ed fixes for Dev changes 2026-04-24 12:12:16 -05:00
Adam Moussa
06bde26aff Wire FollowUp controller for real CRUD
Adds GetById and Delete; switches list filter from completed=bool to
status=string; expands list payload with FK ids, ScheduleStartTime, and
joined CreatedByName so the frontend edit form can prefill and the list
can show who created each item. Requires auth and stamps createdby from
the logged-in user on Create.
2026-04-20 13:49:45 -04:00
Adam Moussa
899ee7b83d Add NTE uplift workflow and vendor PO detail API
Introduces DispatchUpliftRequest entity and tier-gated approval flow:
vendors request a new NTE from the portal, dispatchers approve or deny
in SHOC, and requests above a configurable threshold require a higher
role. Adds DispatchController for PO-centric list/detail used by the
new Vendor POs page. Seeds Dispatcher and Manager roles.
2026-04-20 13:25:56 -04:00
Adam Moussa
b36ccd2758 Merge feature/vendor-portal into dev 2026-04-20 12:45:04 -04:00
Adam Moussa
73be673444 Add vendor portal with token-based authentication
- VendorAccessToken model + unique-index migration; TokenLifetimeDays config
- VendorPortalTokenService: CSPRNG token generation, rotation, revocation
- VendorPortalController: public portal API guarded by X-Vendor-Token header;
  dispatches list/detail, accept, vendor status transitions, cancel request,
  checklist updates, signoffs (vendor + customer), comments with dispatcher
  attribution via AspNetUsers join
- VendorController: portal-token admin endpoints (get / rotate / revoke)
- WorkOrderController: dispatch email now uses vendor portal URL and HTML-encodes
  user fields; AddDispatchComment now stores CommentType='dispatcher' with the
  SHOC user's name so portal can attribute the author
- DispatchPublicController: deprecated per-dispatch GET accept flow returns a
  static 'link no longer active' page (no state mutation)
2026-04-20 12:01:53 -04:00
Adam Moussa
6e0f891ca3 Update TODO with completed items and vendor portal next steps 2026-04-17 16:33:17 -04:00
Adam Moussa
3355df111f Add locationId filter to GetWorkOrderList
- Accept locationId query parameter for server-side location filtering
- Used by dispatch modal to find all WOs at the same site
2026-04-17 16:13:18 -04:00
Adam Moussa
575e8ab6fc Add dispatcher verification as final dispatch gate
- VerifiedBy and VerifiedAt fields on Dispatch model
- VerifyDispatch endpoint validates all checklist items complete + both signoffs present
- Returns missing items list if validation fails
- Sets status to Verified, logs to audit trail
- GetDispatchById includes verifiedBy and verifiedAt
- Migration for new fields
2026-04-17 16:03:34 -04:00
Adam Moussa
81472bf729 Add dispatch sign-offs with signature capture
- DispatchSignoff model (DispatchId, SignoffType, Name, Signature base64, SignatureMethod, SignedAt)
- AddDispatchSignoff endpoint — one per type per dispatch, validates no duplicate
- GetDispatchById includes signoffs in response
- Migration for DispatchSignoffs table
2026-04-17 15:51:20 -04:00
Adam Moussa
c482fa0558 Add vendor accept via email link
- DispatchPublicController with public GET /api/dispatch/accept/{token}
- Updates dispatch status to Acknowledged with timestamp
- Returns styled HTML confirmation page
- Dispatch email now includes green Accept Dispatch button
- Logs vendor acceptance to audit trail
2026-04-17 15:39:50 -04:00
Adam Moussa
3a25fa8559 Add dispatch checklist items with template support
- DispatchChecklistItem model (DispatchId, WorkOrderId, ItemText, IsCompleted, CompletedBy, CompletedAt)
- DispatchToVendor copies template items when TaskListTemplateId provided
- Supports custom checklist items alongside template items
- UpdateChecklistItem endpoint to toggle completion with user name
- AddChecklistItem endpoint for ad-hoc items
- GetDispatchById includes checklist items in response
- Migration for DispatchChecklistItems table
2026-04-17 15:28:52 -04:00
Adam Moussa
be190836a4 Add multi-WO dispatch support with junction table
- DispatchWorkOrder junction table for 1:N dispatch-to-WO relationship
- Make Dispatch.WorkOrderId nullable (backward compat)
- Add AcceptToken and AcknowledgedAt to Dispatch model
- Dispatch_DTO accepts WorkOrderIds array
- DispatchToVendor creates junction rows, email lists all WOs in table
- GetDispatches queries both junction table and direct FK
- GetDispatchById includes workOrders list from junction table
- Migration with DispatchWorkOrder table
2026-04-17 15:16:28 -04:00
Adam Moussa
1f4bd12cd7 Add Task List Templates with CRUD controller
- TaskListTemplate and TaskListTemplateItem models
- TaskListTemplateController: list, get by ID, create with items, update (replace items), soft delete
- Migration for new tables
2026-04-17 14:57:18 -04:00
Adam Moussa
63f76e9b2c Add dispatch detail modal backend + vendor reply sync
- Add DispatchNumber and CompletedDate to Dispatch model
- Add DispatchId to Comments for per-dispatch vendor threads
- GetDispatchById endpoint with vendor communication thread
- UpdateDispatch endpoint for status, NTE, dates, description
- AddDispatchComment endpoint — saves comment + sends email to vendor with sender name
- BackfillDispatchNumbers endpoint for existing dispatches
- SyncVendorReplies endpoint — pulls from DynamoDB VendorReplies table
- Fix reply-to address to include dispatch number
- Include sender name in dispatch and comment emails
2026-04-17 14:01:03 -04:00
Adam Moussa
17590daf9d Add project TODO tracking deferred work items
- Dispatch detail modal, Front API integration, vendor communication
- Missing pages (profile, settings, calendar)
- Dashboard analytics, PO enhancements, deployment migration
2026-04-17 13:08:12 -04:00
Adam Moussa
f89db3da52 Add server-side status and assignee filters to GetWorkOrderList
- Accept status and assignee query parameters
- Filter in SQL before counting and paginating
- Supports __unassigned for unassigned work orders
- Accurate totalCount reflects filtered results
2026-04-17 12:57:37 -04:00
Adam Moussa
7d764f966d Add server-side sorting to GetWorkOrderList
- Accept sortBy and sortDir query parameters
- Supports sorting by: number, title, location, priority, status,
  createdDate, assignedTo, lastUpdated
- Default sort: lastUpdated desc
- Sort applied in SQL before pagination
2026-04-17 12:50:23 -04:00
Adam Moussa
091390fcad Calculate lastUpdated from comments, audit log, and modification time
- Takes the max of: LastModificationTime, latest comment CreatedDate,
  latest audit log CreatedAt
- Reflects any comment (customer/vendor/internal) or status change
2026-04-17 12:35:14 -04:00
Adam Moussa
0a564499f5 Add lastUpdated field to work order list response 2026-04-17 12:33:30 -04:00
Adam Moussa
7ef7205681 Include InternalWONumber in work order list search
- Search now matches against internal WO number, customer WO number,
  title, and location name/title
2026-04-17 12:31:00 -04:00
Adam Moussa
b072d1fa2d Extract zip code from location address as fallback for distance calc
- Regex extracts 5-digit zip from address string when ZipCode field is empty
- Enables distance calculation for synced locations without structured zip data
2026-04-17 11:59:34 -04:00
Adam Moussa
521bb0fba2 Add zip-to-zip distance calculation for vendor dispatch
- ZipCodeDistance helper with Haversine formula on 33K US zip code centroids
- Loaded as singleton from wwwroot/zipcodes.csv on startup
- Vendor Dropdown endpoint accepts siteZip, returns address and distanceMiles
- Vendors sorted by distance when siteZip provided
- Include locationZip in GetWorkorderById response
2026-04-17 11:56:06 -04:00
Adam Moussa
ee69a1863a Add vendor scheduled date to dispatch workflow
- Add ScheduledDate field to Dispatch model and DTO
- Save scheduled date when dispatching to vendor
- Include ScheduledDate in dispatch response projections
2026-04-17 11:47:00 -04:00
Adam Moussa
d24c4643f5 Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
  HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
Adam Moussa
05b36ef7dd Add configurable dropdowns, new WO fields, and seed data
- Create DropdownOption model with Category, Value, ParentValue for Trade/SubTrade/Problem
- Add DropdownOptionsController with CRUD + ByCategory endpoint with parent filtering
- Add Problem, Trade, SubTrade, VendorNTE, ScheduledDate, CompletedDate, Source to WorkOrder
- Update EditWorkorder_DTO and GetWorkorderById with new fields
- Audit log tracks changes to all new fields
- Seed default Trades (10), SubTrades (20), and Problems (11) on startup
2026-04-17 10:40:38 -04:00
Adam Moussa
78c7850fb2 Add DashboardController with Stats endpoint
- Returns total, open, notDispatched, completed work order counts
2026-04-16 19:25:42 -04:00
Adam Moussa
8959efc06b Add ChangeAssignment endpoint with audit logging
- POST /api/WorkOrder/ChangeAssignment?id=&assignTo=
- Logs old and new user names in audit trail
- Returns updated assignment name
2026-04-16 19:04:59 -04:00
Adam Moussa
d07e24e419 Add InternalWONumber field with auto-generation
- Add InternalWONumber to WorkOrder model
- Auto-generate 8-digit sequential numbers starting at 10000001
- Generate on new work order creation and DynamoDB sync
- Include in GetWorkorderById and GetWorkOrderList responses
- Add BackfillInternalWONumbers endpoint for existing records
- Add migration for new column
2026-04-16 18:42:58 -04:00
Adam Moussa
be70c84add Make Comments_DTO fields nullable for JSON endpoint compatibility
- Text, UserId, CreatedDate are set server-side, not required from client
2026-04-16 18:24:04 -04:00
Adam Moussa
18ca737307 Add JSON-based AddCommentJson endpoint
- Accepts JSON body instead of FormData for simple text comments
- Returns new comment with user name for immediate UI update
- Defaults to internal comment type
2026-04-16 18:22:34 -04:00
Adam Moussa
e1f57d3fb2 Add comment types, audit logging, and WorkOrderAuditLog table
- Add CommentType field to Comments (customer, vendor, internal)
- Add Status field to EditWorkorder_DTO
- Create WorkOrderAuditLog model tracking field-level changes
- Log all field changes on work order edit and status change
- Include commentType and auditLog in GetWorkorderById response
- Set CommentType=customer on synced comments from DynamoDB
- Add BackfillCommentTypes endpoint for existing data
2026-04-16 18:09:57 -04:00
Adam Moussa
c69cef7fa9 Add HttpPut route to EditWorkorder endpoint
- services/api.js sends PUT for updates, controller only accepted POST
- Add [HttpPut("{id:int}")] alongside existing [HttpPost]
2026-04-16 17:44:00 -04:00
Adam Moussa
cc87039a77 Include commenter and recordType in GetWorkorderById comments response 2026-04-16 17:41:11 -04:00
Adam Moussa
5a4d8e2240 Add description, createdDate, locationId to GetWorkorderById response 2026-04-16 17:37:21 -04:00
Adam Moussa
60fc933da7 Add lowercase route aliases for all controllers
- services/api.js uses lowercase routes (/api/workorders, /api/employees, etc.)
- Add matching [Route] attributes to WorkOrder, Employee, Contact, Account, FollowUp, PMSchedule controllers
- Add [HttpGet("{id:int}")] to WorkOrderController for REST-style get by ID
2026-04-16 17:32:28 -04:00
Adam Moussa
6f8f67735a Add DynamoDB sync bridge for workorder-ingest pipeline
- Add ExternalWorkOrderId, Customer, SiteCode, Building, Severity,
  DateReported, ScheduledStart, SourceEmailS3Key to WorkOrder model
- Add Commenter, RecordType, ExternalCommentId to Comments model
- Add AWSSDK.DynamoDBv2 package
- Create SyncController with endpoints:
  - POST /api/Sync/WorkOrders — scans DynamoDB WorkOrders table, upserts into SQL
  - POST /api/Sync/Comments — scans DynamoDB WorkOrderComments, creates Comments
  - POST /api/Sync/All — runs both
- Auto-creates Locations from site_code/building/address during sync
- Maps DynamoDB status values to SHOC status strings
- Maps severity to SHOC priority format (Sev N)
- Deduplicates on ExternalWorkOrderId / ExternalCommentId
2026-04-16 17:29:52 -04:00
Adam Moussa
93fa76a5aa Handle string-to-int AccountId conversion in Location DTOs
- Frontend sends accountId as empty string when not set
- Change DTO AccountId to string with GetAccountId() parser
- Prevents JSON deserialization error on empty/non-numeric values
2026-04-16 17:15:28 -04:00
Adam Moussa
5a20e55138 Add lowercase route alias for LocationController
- services/api.js uses /api/locations (lowercase), controller was only matching /api/Location
- Add [Route("api/locations")] to support both casings
2026-04-16 17:11:36 -04:00
Adam Moussa
534f97d6f1 Add LocationController with full CRUD and extended model
- Extend Locations model with Address, City, State, ZipCode, Phone, Contact, ContactEmail, Status, AccountId
- Update Location DTOs to match new fields
- Create LocationController with paged list, get by ID, create, update, delete
- Support both REST-style routes (/api/Location/{id}) and legacy query routes (/api/Location/DeleteLocation?id=)
- Add migration for new Location columns
2026-04-16 17:09:59 -04:00
Adam Moussa
51b52a7fb5 Add migration to sync Contacts table with model
- Generate SyncContactsAndModels migration for missing columns
- Adds AccountId, Address fields, social URLs, LocationId to Contacts table
2026-04-16 16:54:36 -04:00
Adam Moussa
dbb31ca7d7 Fix Contacts.Name references throughout Blazor project
- Replace all Contacts.Name with (FirstName + " " + LastName) pattern
- Fix ContactService, Index.razor, QuotesPDF, QuotesDetails, CreateQuotes
- Fix WorkOrderPDF, Viewworkorder, UserViewWordOrder, Addworkorders
- Fix ViewTemplate, AddTemplate, Templates settings pages
- Blazor project now builds successfully
2026-04-16 16:54:28 -04:00
Adam Moussa
493cff00fb Fix pagination to use database-side Skip/Take instead of loading all rows
- Move .ToList() after .Skip().Take() in ContactController, QuotesController, SettingsController
- Prevents loading entire tables into memory for paginated queries
2026-04-16 16:54:18 -04:00
Adam Moussa
483b765d69 Remove sample WeatherForecastController 2026-04-16 16:54:10 -04:00
Adam Moussa
e8c400a20a Add null safety checks across all API controllers
- Add null checks after FirstOrDefault() before property access
- Guard navigation property access (Locations, AssignToUser, ApplicationUser, POC, Contacts)
- Null-coalesce string properties in search/filter LINQ queries
- Protect Sum() on nullable LineItems collections
- Change FirstAsync to FirstOrDefaultAsync with null guard in UserController
- Add null check on model.Attachments iteration in EditWorkorder
2026-04-16 16:33:42 -04:00
Adam Moussa
128ae44a0d Fix login null check order and strengthen password reset code
- Reorder null check: check user != null before accessing user.IsDeleted
- Increase password reset code from 4 digits to 6 digits (10K to 1M combinations)
2026-04-16 16:33:09 -04:00
Adam Moussa
486233076c Replace hardcoded SMTP credentials with SendGrid service
- Remove inline SMTP method with codevoir.com credentials
- Inject existing SendMessage helper that uses SendGrid via config
- Clean up unused using directives
2026-04-16 16:25:11 -04:00
Adam Moussa
0e3cb761c9 Configure local development environment
- Generalize appsettings.json by removing hardcoded credentials
- Add local connection string and JWT config to appsettings.Development.json
- Add EF Core Design package for running migrations locally
- Seed admin user on startup in development
- Skip HTTPS redirect in development for proxy compatibility
- Gitignore appsettings.Development.json
2026-04-16 16:25:03 -04:00
164 changed files with 86061 additions and 769 deletions

3
.gitignore vendored
View file

@ -362,3 +362,6 @@ MigrationBackup/
# Fody - auto-generated XML schema
FodyWeavers.xsd
/SeaHavenIndustries/wwwroot/ChromeEx/Win_x64_1000083_chrome-win/chrome-win
appsettings.Development.json
.DS_Store

View file

@ -7,7 +7,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AWSSDK.DynamoDBv2" Version="4.0.17.9" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="SendGrid" Version="9.29.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageReference Include="PasswordGenerator" Version="2.1.0" />
@ -15,6 +20,8 @@
<ItemGroup>
<ProjectReference Include="..\Data.SeaHavenIndustries\Data.SeaHavenIndustries.csproj" />
<ProjectReference Include="..\SeaHaven.Services\SeaHaven.Services.csproj" />
<ProjectReference Include="..\SeaHaven.DataServices\SeaHaven.DataServices.csproj" />
</ItemGroup>
</Project>

View file

@ -2,16 +2,19 @@
using Data.SeaHavenIndustries;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SeaHaven.Services.Interfaces;
using SeaHaven.Services.DTOs;
[ApiController]
[Route("api/Account")]
[Route("api/accounts")]
public class AccountController : ControllerBase
{
private readonly ApplicationDbContext _db;
private readonly IAccountService _accountService;
public AccountController(ApplicationDbContext db)
public AccountController(IAccountService accountService)
{
_db = db;
_accountService = accountService;
}
// -----------------------------------
@ -22,34 +25,16 @@ public class AccountController : ControllerBase
{
try
{
var model = new Accounts
var createDto = new CreateAccountDTO
{
Name = dto.Name,
Type = dto.Type,
Importance = dto.Importance,
TerritoryId = dto.TerritoryId,
OwnerEmployeeId = dto.OwnerEmployeeId,
PrivacySetting = dto.PrivacySetting,
About = dto.About,
Tags = dto.Tags,
Name = dto.Name ?? throw new ArgumentException("Name is required"),
Phone = dto.Phone,
Email = dto.Email,
IndustryId = dto.IndustryId,
TimeZone = dto.TimeZone,
Website = dto.Website,
CurrencyCode = dto.CurrencyCode,
ServiceInstructions = dto.ServiceInstructions,
FacebookUrl = dto.FacebookUrl,
LinkedInUrl = dto.LinkedInUrl,
TwitterUrl = dto.TwitterUrl,
CreatedDate = DateTime.UtcNow
Website = dto.Website
};
_db.Accounts.Add(model);
await _db.SaveChangesAsync();
var userId = User.Identity?.Name ?? "system";
await _accountService.CreateAccountAsync(createDto, userId);
return Ok(new DataResponse
{
@ -75,34 +60,16 @@ public class AccountController : ControllerBase
{
try
{
var account = await _db.Accounts.FirstOrDefaultAsync(a => a.Id == dto.Id);
if (account == null)
return BadRequest(new Response { Status = "Error", Message = "Account not found." });
var updateDto = new UpdateAccountDTO
{
Name = dto.Name,
Phone = dto.Phone,
Email = dto.Email,
Website = dto.Website
};
account.Name = dto.Name;
account.Type = dto.Type;
account.Importance = dto.Importance;
account.TerritoryId = dto.TerritoryId;
account.OwnerEmployeeId = dto.OwnerEmployeeId;
account.PrivacySetting = dto.PrivacySetting;
account.About = dto.About;
account.Tags = dto.Tags;
account.Phone = dto.Phone;
account.Email = dto.Email;
account.IndustryId = dto.IndustryId;
account.TimeZone = dto.TimeZone;
account.Website = dto.Website;
account.CurrencyCode = dto.CurrencyCode;
account.ServiceInstructions = dto.ServiceInstructions;
account.FacebookUrl = dto.FacebookUrl;
account.LinkedInUrl = dto.LinkedInUrl;
account.TwitterUrl = dto.TwitterUrl;
account.LastModificationTime = DateTime.UtcNow;
await _db.SaveChangesAsync();
var userId = User.Identity?.Name ?? "system";
await _accountService.UpdateAccountAsync(dto.Id, updateDto, userId);
return Ok(new DataResponse
{
@ -128,12 +95,8 @@ public class AccountController : ControllerBase
{
try
{
var account = await _db.Accounts.FirstOrDefaultAsync(a => a.Id == id);
if (account == null)
return BadRequest(new Response { Status = "Error", Message = "Account not found." });
_db.Accounts.Remove(account);
await _db.SaveChangesAsync();
var userId = User.Identity?.Name ?? "system";
await _accountService.DeleteAccountAsync(id, userId);
return Ok(new DataResponse
{
@ -155,49 +118,28 @@ public class AccountController : ControllerBase
// GET: api/Account/GetAccountList?search=&page=&pageSize=
// -----------------------------------
[HttpGet("GetAccountList")]
public IActionResult GetAccountList(string? search = "", int page = 1, int pageSize = 12)
public async Task<IActionResult> GetAccountList(string? search = "", int page = 1, int pageSize = 12)
{
search ??= "";
var pagedResult = await _accountService.GetAccountsPagedAsync(page, pageSize, search);
var query = _db.Accounts
.Where(a => a.IsDeleted == false || a.IsDeleted == null);
if (!string.IsNullOrWhiteSpace(search))
var data = pagedResult.Items.Select(a => new
{
var s = search.ToLower();
query = query.Where(a =>
(a.Name ?? "").ToLower().Contains(s) ||
(a.Email ?? "").ToLower().Contains(s) ||
(a.Phone ?? "").Contains(s) ||
(a.Website ?? "").ToLower().Contains(s)
);
}
var totalCount = query.Count();
var data = query
.OrderByDescending(a => a.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(a => new
{
a.Id,
a.Name,
a.Type,
a.Phone,
a.Email,
Industry = a.IndustryId,
a.Website
})
.ToList();
Id = a.Id,
Name = a.Name,
Type = a.Type ?? "",
Phone = a.Phone,
Email = a.Email,
Industry = (int?)null, // Map if needed
Website = a.Website
});
var viewModel = new Pagination_DTO
{
Data = data,
PageNumber = page,
PageSize = pageSize,
TotalCount = totalCount,
TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize)
TotalCount = pagedResult.TotalCount,
TotalPages = (int)Math.Ceiling(pagedResult.TotalCount / (double)pageSize)
};
return Ok(viewModel);

View file

@ -4,6 +4,8 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SeaHaven.Services.Interfaces;
using SeaHaven.Services.DTOs;
namespace Api.SeaHavenIndustries.Controllers
{
@ -13,12 +15,12 @@ namespace Api.SeaHavenIndustries.Controllers
public class AssetController : Controller
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly ApplicationDbContext _db;
public AssetController(UserManager<ApplicationUser> userManager, ApplicationDbContext db)
private readonly IAssetService _assetService;
public AssetController(UserManager<ApplicationUser> userManager, IAssetService assetService)
{
_userManager = userManager;
_db = db;
_assetService = assetService;
}
[HttpPost]
@ -27,17 +29,15 @@ namespace Api.SeaHavenIndustries.Controllers
{
try
{
Assets model = new Assets();
model.AssetCode = asset.AssetCode;
model.Name = asset.Name;
model.Description = asset.Description;
model.AccountId = asset.AccountId;
model.LocationId = asset.LocationId;
model.IsActive = asset.IsActive;
_db.Assets.Add(model);
await _db.SaveChangesAsync();
var createDto = new CreateAssetDTO
{
Name = asset.Name ?? throw new ArgumentException("Name is required"),
LocationId = asset.LocationId
};
var userId = User.Identity?.Name ?? "system";
await _assetService.CreateAssetAsync(createDto, userId);
return Ok(new DataResponse { Message = "Asset Created Successfully", Status = "200" });
}
catch (Exception ex)
@ -52,23 +52,15 @@ namespace Api.SeaHavenIndustries.Controllers
{
try
{
var exist = _db.Assets.Where(w => w.Id == asset.Id).FirstOrDefault();
if (exist == null)
var updateDto = new UpdateAssetDTO
{
return BadRequest(new Response { Status = "Error", Message = "Asset not found!" });
}
Name = asset.Name,
LocationId = asset.LocationId
};
var userId = User.Identity?.Name ?? "system";
await _assetService.UpdateAssetAsync(asset.Id, updateDto, userId);
exist.AssetCode = asset.AssetCode;
exist.Name = asset.Name;
exist.Description = asset.Description;
exist.AccountId = asset.AccountId;
exist.LocationId = asset.LocationId;
exist.IsActive = asset.IsActive;
_db.Assets.Update(exist);
await _db.SaveChangesAsync();
return Ok(new DataResponse { Message = "Asset Updated Successfully", Status = "200" });
}
catch (Exception ex)
@ -83,18 +75,8 @@ namespace Api.SeaHavenIndustries.Controllers
{
try
{
var exist = _db.Assets.Where(w => w.Id == id).FirstOrDefault();
if (exist != null)
{
_db.Remove(exist);
}
else
{
return BadRequest(new Response { Status = "Error", Message = "Asset not found!" });
}
await _db.SaveChangesAsync();
var userId = User.Identity?.Name ?? "system";
await _assetService.DeleteAssetAsync(id, userId);
return Ok(new DataResponse { Message = "Asset Deleted Successfully", Status = "200" });
}
catch (Exception ex)
@ -109,17 +91,13 @@ namespace Api.SeaHavenIndustries.Controllers
{
try
{
var asset = await _db.Assets
.Include(a => a.Account)
.Include(a => a.Location)
.Where(w => w.Id == id)
.FirstOrDefaultAsync();
var asset = await _assetService.GetAssetByIdWithDetailsAsync(id);
if (asset == null)
{
return NotFound(new Response { Status = "Error", Message = "Asset not found" });
}
return Ok(asset);
}
catch (Exception ex)
@ -130,48 +108,25 @@ namespace Api.SeaHavenIndustries.Controllers
[HttpGet]
[Route("GetAssetList")]
public IActionResult GetAssetList(string? search = "", int page = 1, int pageSize = 10)
public async Task<IActionResult> GetAssetList(string? search = "", int page = 1, int pageSize = 10)
{
// Build the query with search filter
var query = _db.Assets
.Include(a => a.Account)
.Include(a => a.Location)
.Where(s =>
string.IsNullOrEmpty(search) ||
(s.Name != null && s.Name.ToLower().Contains(search.ToLower())) ||
(s.AssetCode != null && s.AssetCode.ToLower().Contains(search.ToLower())) ||
(s.Description != null && s.Description.ToLower().Contains(search.ToLower()))
);
var pagedResult = await _assetService.GetAssetsPagedAsync(page, pageSize, search);
// Get total count AFTER applying search filter
var totalCount = query.Count();
// Get paginated data
var data = query
.Select(n => new
{
n.Id,
n.AssetCode,
n.Name,
n.Description,
n.IsActive,
AccountName = n.Account != null ? n.Account.Name : "",
LocationName = n.Location != null ? n.Location.Name : ""
})
.OrderByDescending(d => d.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToList();
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
var data = pagedResult.Items.Select(a => new
{
a.Id,
a.Name,
LocationName = a.LocationName ?? "",
a.CreatedDate
});
var viewModel = new Pagination_DTO
{
Data = data,
PageNumber = page,
PageSize = pageSize,
TotalCount = totalCount,
TotalPages = totalPages
TotalCount = pagedResult.TotalCount,
TotalPages = (int)Math.Ceiling(pagedResult.TotalCount / (double)pageSize)
};
return Ok(viewModel);

View file

@ -4,13 +4,11 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Mail;
using System.Net;
using System.Security.Claims;
using System.Text;
using Api.SeaHavenIndustries.DTOs;
using Api.SeaHavenIndustries.Helper;
using Microsoft.EntityFrameworkCore;
using System.Diagnostics;
namespace Api.SeaHavenIndustries.Controllers
{
@ -21,12 +19,14 @@ namespace Api.SeaHavenIndustries.Controllers
private readonly UserManager<ApplicationUser> _userManager;
private readonly IConfiguration _configuration;
private readonly ApplicationDbContext _db;
private readonly SendMessage _sendMessage;
public AuthenticationController(UserManager<ApplicationUser> userManager, IConfiguration configuration, ApplicationDbContext db, IWebHostEnvironment webHostEnvironment, IHttpContextAccessor httpContext)
public AuthenticationController(UserManager<ApplicationUser> userManager, IConfiguration configuration, ApplicationDbContext db, SendMessage sendMessage)
{
_userManager = userManager;
_configuration = configuration;
_db = db;
_sendMessage = sendMessage;
}
[AllowAnonymous]
@ -34,36 +34,45 @@ namespace Api.SeaHavenIndustries.Controllers
[Route("login")]
public async Task<IActionResult> Login([FromBody] LoginModel model)
{
var user = await _userManager.FindByNameAsync(model.Username ?? "");
if (user.IsDeleted != true && user != null && await _userManager.CheckPasswordAsync(user, model.Password ?? ""))
try
{
// Standard login without 2FA
var userRoles = await _userManager.GetRolesAsync(user);
var authClaims = new List<Claim>
var user = await _userManager.FindByNameAsync(model.Username ?? "");
if (user != null && user.IsDeleted != true && await _userManager.CheckPasswordAsync(user, model.Password ?? ""))
{
// Standard login without 2FA
var userRoles = await _userManager.GetRolesAsync(user);
var authClaims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.UserName ?? ""),
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
foreach (var userRole in userRoles)
{
authClaims.Add(new Claim(ClaimTypes.Role, userRole));
foreach (var userRole in userRoles)
{
authClaims.Add(new Claim(ClaimTypes.Role, userRole));
}
var token = GetToken(authClaims);
return Ok(new
{
token = new JwtSecurityTokenHandler().WriteToken(token),
expiration = token.ValidTo,
email = user.Email,
userRoles = userRoles.FirstOrDefault(),
phoneNumber = user.PhoneNumber,
fullname = user.FirstName + " " + user.LastName,
id = user.Id
});
}
var token = GetToken(authClaims);
return Ok(new
{
token = new JwtSecurityTokenHandler().WriteToken(token),
expiration = token.ValidTo,
email = user.Email,
userRoles = userRoles.FirstOrDefault(),
phoneNumber = user.PhoneNumber,
fullname = user.FirstName + " " + user.LastName,
id = user.Id
});
}
// Invalid credentials
return Unauthorized();
// Invalid credentials
return Unauthorized();
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
}
[Route("ChangePassword")]
@ -150,7 +159,7 @@ namespace Api.SeaHavenIndustries.Controllers
_db.ForgetPasswordCodes.Add(forgetPasswordCode);
_db.SaveChanges();
string body = $"Your Password Reset Code is: " + forgetPasswordCode.Code;
SendEMail(user.Email, "Forget Password Request.", body);
await _sendMessage.SendEMail(user.Email, "Forget Password Request.", body);
return Ok(new Response { Status = "Success ", Message = "Please check your email for code" });
}
else
@ -211,42 +220,10 @@ namespace Api.SeaHavenIndustries.Controllers
}
}
[HttpGet()]
public bool SendEMail(string emailid, string subject, string body)
{
try
{
MailMessage mail = new MailMessage();
mail.From = new MailAddress("infoesquall@codevoir.com"); //IMPORTANT: This must be same as your smtp authentication address.
mail.To.Add(emailid);
//set the content
mail.Subject = subject;
mail.Body = body;
mail.IsBodyHtml = true;
//send the message
SmtpClient smtp = new SmtpClient("mail.codevoir.com");
//IMPORANT: Your smtp login email MUST be same as your FROM address.
NetworkCredential Credentials = new NetworkCredential("infoesquall@codevoir.com", "Abc123!@#");
smtp.UseDefaultCredentials = false;
smtp.Credentials = Credentials;
smtp.Port = 8889; //25 alternative port number is 8889
smtp.EnableSsl = false;
smtp.Send(mail);
return true;
}
catch (Exception ex)
{
return false;
}
}
private Random _random = new Random();
private string GenerateRandomNo()
{
return _random.Next(0, 9999).ToString("D4");
return _random.Next(0, 999999).ToString("D6");
}
#endregion
}

View file

@ -30,7 +30,7 @@ namespace Api.SeaHavenIndustries.Controllers
try
{
var allUsersQuery = _db.Users
.Where(w => w.FirstName.ToLower().Contains(search.ToLower()))
.Where(w => (w.FirstName ?? "").ToLower().Contains((search ?? "").ToLower()))
.OrderByDescending(d => d.Id);
var totalCount = allUsersQuery.Count();

View file

@ -5,20 +5,28 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SendGrid.Helpers.Mail;
using SeaHaven.Services.Interfaces;
namespace Api.SeaHavenIndustries.Controllers
{
//[Authorize] // Removed for testing
[ApiController]
[Route("api/Contact")]
[Route("api/contacts")]
public class ContactController : Controller
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly ApplicationDbContext _db;
public ContactController(UserManager<ApplicationUser> userManager, ApplicationDbContext db)
private readonly IContactService _contactService;
private readonly ILocationService _locationService;
public ContactController(
UserManager<ApplicationUser> userManager,
IContactService contactService,
ILocationService locationService)
{
_userManager = userManager;
_db = db;
_contactService = contactService;
_locationService = locationService;
}
[HttpPost]
[Route("AddContacts")]
@ -26,27 +34,10 @@ namespace Api.SeaHavenIndustries.Controllers
{
try
{
Contacts model = new Contacts();
model.Title = contacts.Title;
model.Owner = contacts.Owner;
model.FirstName = contacts.FirstName;
model.MiddleName = contacts.MiddleName;
model.LastName = contacts.LastName;
model.ContactType = contacts.ContactType;
model.PhoneNumber = contacts.PhoneNumber;
model.Email = contacts.Email;
model.Address1 = contacts.Address1;
model.Address2 = contacts.Address2;
model.City = contacts.City;
model.State = contacts.State;
model.Zip = contacts.Zip;
model.FacebookUrl = contacts.FacebookUrl;
model.LinkedInUrl = contacts.LinkedInUrl;
model.TwitterUrl = contacts.TwitterUrl;
model.LocationId = contacts.LocationId;
model.AccountId = contacts.AccountId;
_db.Contacts.Add(model);
await _db.SaveChangesAsync();
// ✅ Use DTO mapping method instead of manual property assignment
var userId = _userManager.GetUserId(User) ?? "system";
await _contactService.CreateContactAsync(contacts.ToServiceCreateDTO(), userId);
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
}
catch (Exception ex)
@ -61,53 +52,39 @@ namespace Api.SeaHavenIndustries.Controllers
{
try
{
var exist = _db.Contacts.Where(w => w.Id == contacts.Id).FirstOrDefault();
// ✅ Use DTO mapping method instead of manual property assignment
var userId = _userManager.GetUserId(User) ?? "system";
await _contactService.UpdateContactAsync(contacts.Id, contacts.ToServiceUpdateDTO(), userId);
exist.Title = contacts.Title;
exist.Owner = contacts.Owner;
exist.FirstName = contacts.FirstName;
exist.MiddleName = contacts.MiddleName;
exist.LastName = contacts.LastName;
exist.ContactType = contacts.ContactType;
exist.PhoneNumber = contacts.PhoneNumber;
exist.Email = contacts.Email;
exist.Address1 = contacts.Address1;
exist.Address2 = contacts.Address2;
exist.City = contacts.City;
exist.State = contacts.State;
exist.Zip = contacts.Zip;
exist.FacebookUrl = contacts.FacebookUrl;
exist.LinkedInUrl = contacts.LinkedInUrl;
exist.TwitterUrl = contacts.TwitterUrl;
exist.LocationId = contacts.LocationId;
exist.AccountId = contacts.AccountId;
_db.Contacts.Update(exist);
await _db.SaveChangesAsync();
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
}
catch (KeyNotFoundException)
{
return BadRequest(new Response { Status = "Error", Message = "Contact not found" });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
[HttpGet]
[Route("DeleteContacts")]
public async Task<IActionResult> DeleteContacts(int id)
{
try
{
var exist = _db.Contacts.Where(w => w.Id == id).FirstOrDefault();
if (exist != null)
{
_db.Remove(exist);
}
else
{
return BadRequest(new Response { Status = "Error", Message = "ID not matched!" });
}
await _db.SaveChangesAsync();
var userId = _userManager.GetUserId(User) ?? "system";
// ✅ Call Service layer (not DataService directly)
await _contactService.DeleteContactAsync(id, userId);
return Ok(new DataResponse { Message = "Removed Successfully", Status = "200" });
}
catch (KeyNotFoundException)
{
return BadRequest(new Response { Status = "Error", Message = "ID not matched!" });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
@ -115,32 +92,21 @@ namespace Api.SeaHavenIndustries.Controllers
}
[HttpGet]
[Route("GetContactList")]
public IActionResult GetContactList(string? search ="", int page = 1, int pageSize = 10)
public async Task<IActionResult> GetContactList(string? search ="", int page = 1, int pageSize = 10)
{
var totalCount = _db.Contacts.Count();
var data = _db.Contacts.Where(s=>
(s.FirstName != null && s.FirstName.ToLower().Contains(search.ToLower())) ||
(s.LastName != null && s.LastName.ToLower().Contains(search.ToLower())) ||
(s.Email != null && s.Email.ToLower().Contains(search.ToLower())) ||
(s.PhoneNumber != null && s.PhoneNumber.Contains(search.ToLower()))
).Select(n => new
{
n.Id,
Name = (n.FirstName + " " + n.LastName).Trim(),
email = n.Email,
phone = n.PhoneNumber
}).OrderByDescending(d => d.Id).ToList().Skip((page - 1) * pageSize)
.Take(pageSize);
// Calculate the number of pages
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
// ✅ Call Service layer
var pagedResult = await _contactService.GetContactsPagedAsync(page, pageSize, search);
// ✅ Use DTO mapping method (NO manual transformation in controller)
var data = ContactListItem_DTO.FromServiceDTOs(pagedResult.Items);
var totalPages = (int)Math.Ceiling(pagedResult.TotalCount / (double)pageSize);
// Create a view model containing the paged items and pagination metadata
var viewModel = new Pagination_DTO
{
Data = data,
PageNumber = page,
PageSize = pageSize,
TotalCount = totalCount,
TotalCount = pagedResult.TotalCount,
TotalPages = totalPages
};
return Ok(viewModel);
@ -150,52 +116,54 @@ namespace Api.SeaHavenIndustries.Controllers
[Route("GetContacts")]
public async Task<IActionResult> GetContacts()
{
var data = await _db.Contacts.ToListAsync();
// ✅ Call Service layer
var data = await _contactService.GetAllContactsAsync();
return Ok(data);
}
[HttpGet]
[Route("GetLocations")]
public async Task<IActionResult> GetLocations()
{
var data = await _db.Locations.Select(s => new LocationViewModel
{
Id = s.Id,
Name = s.Title + " - " + s.Name
}).ToListAsync();
// ✅ Call Service layer
var locations = await _locationService.GetAllLocationsAsync();
// ✅ Use DTO mapping method (NO manual transformation in controller)
var data = LocationViewModel_DTO.FromServiceDTOs(locations);
return Ok(data);
}
// TODO: ARCHITECTURAL VIOLATION - These methods should NOT be in ContactController
// GetCategories should be in a CategoryController with ICategoryService
// GetUsers should be in a UserController with IUserService
// For now, commenting out to prevent direct database access
[HttpGet]
[Route("GetCategories")]
public async Task<IActionResult> GetCategories()
{
var data = await _db.Categories.ToListAsync();
return Ok(data);
// ❌ TEMPORARY: Needs ICategoryService
return StatusCode(501, new Response { Status = "Error", Message = "Not Implemented - Needs CategoryService" });
}
[HttpGet]
[Route("GetUsers")]
public async Task<IActionResult> GetUsers()
{
var filteredUsers = await (from user in _db.Users
where !(from userRole in _db.UserRoles
join role in _db.Roles on userRole.RoleId equals role.Id
where role.Name == "Admin" || role.Name == "Manager"
select userRole.UserId).Contains(user.Id)
select user).ToListAsync();
return Ok(filteredUsers);
// ❌ TEMPORARY: Needs IUserService
return StatusCode(501, new Response { Status = "Error", Message = "Not Implemented - Needs UserService" });
}
// TODO: ARCHITECTURAL VIOLATION - Location CRUD methods should NOT be in ContactController
// These should be in LocationController with ILocationService
[HttpPost]
[Route("AddLocation")]
public async Task<IActionResult> AddLocation(Location_DTO model)
{
try
{
Locations locations = new Locations();
locations.Title = model.Title;
locations.Name = model.Name;
_db.Locations.Add(locations);
await _db.SaveChangesAsync();
// ✅ Use Service layer with DTO mapping
var userId = _userManager.GetUserId(User) ?? "system";
await _locationService.CreateLocationAsync(model.ToServiceCreateDTO(), userId);
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
}
catch (Exception ex)
@ -203,26 +171,22 @@ namespace Api.SeaHavenIndustries.Controllers
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
[HttpPost]
[Route("EditLocation")]
public async Task<IActionResult> EditLocation(EditLocation_DTO model)
{
try
{
var exist = _db.Locations.Where(w => w.Id == model.Id).FirstOrDefault();
if (exist != null)
{
exist.Title = model.Title;
exist.Name = model.Name;
}
else
{
return BadRequest(new Response { Status = "Error", Message = "ID not matched!" });
}
_db.Locations.Update(exist);
await _db.SaveChangesAsync();
// ✅ Use Service layer with DTO mapping
var userId = _userManager.GetUserId(User) ?? "system";
await _locationService.UpdateLocationAsync(model.Id, model.ToServiceUpdateDTO(), userId);
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
}
catch (KeyNotFoundException)
{
return BadRequest(new Response { Status = "Error", Message = "ID not matched!" });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
@ -235,20 +199,15 @@ namespace Api.SeaHavenIndustries.Controllers
{
try
{
var data1 = await _db.Templates.ToListAsync();
_db.Templates.RemoveRange(data1);
var data = await _db.Locations.Where(w => w.Id == Id).FirstOrDefaultAsync();
if (data != null)
{
_db.Locations.Remove(data ?? new Locations());
}
else
{
return BadRequest(new Response { Status = "Error", Message = "ID not matched!" });
}
await _db.SaveChangesAsync();
// ✅ Use Service layer
var userId = _userManager.GetUserId(User) ?? "system";
await _locationService.DeleteLocationAsync(Id, userId);
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
}
catch (KeyNotFoundException)
{
return BadRequest(new Response { Status = "Error", Message = "ID not matched!" });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
@ -257,36 +216,25 @@ namespace Api.SeaHavenIndustries.Controllers
[HttpGet]
[Route("GetAddressbook")]
public IActionResult GetAddressbook(string? search = "", int page = 1, int pageSize = 10)
public async Task<IActionResult> GetAddressbook(string? search = "", int page = 1, int pageSize = 10)
{
var totalCount = _db.Locations.Count();
var data = _db.Locations.Where(w=> w.Name.ToLower().Contains(search.ToLower()) || w.Title.ToLower().Contains(search.ToLower())).Select(n => new
{
n.Id,
n.Name,
n.Title
}).OrderByDescending(d => d.Id).ToList().Skip((page - 1) * pageSize)
.Take(pageSize);
// Calculate the number of pages
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
// ✅ Call Service layer
var pagedResult = await _locationService.GetLocationsPagedAsync(page, pageSize, search);
// ✅ Use DTO mapping method (NO manual transformation in controller)
var data = LocationListItem_DTO.FromServiceDTOs(pagedResult.Items);
var totalPages = (int)Math.Ceiling(pagedResult.TotalCount / (double)pageSize);
// Create a view model containing the paged items and pagination metadata
var viewModel = new Pagination_DTO
{
Data = data,
PageNumber = page,
PageSize = pageSize,
TotalCount = totalCount,
TotalCount = pagedResult.TotalCount,
TotalPages = totalPages
};
return Ok(viewModel);
}
public class LocationViewModel
{
public int? Id { get; set; }
public string? Name { get; set; }
}
}
}

View file

@ -0,0 +1,39 @@
using Data.SeaHavenIndustries;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Api.SeaHavenIndustries.Controllers
{
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class DashboardController : Controller
{
private readonly ApplicationDbContext _db;
public DashboardController(ApplicationDbContext db)
{
_db = db;
}
[HttpGet("Stats")]
public async Task<IActionResult> GetStats()
{
var query = _db.workOrders.Where(w => w.istemplate != true);
var total = await query.CountAsync();
var open = await query.CountAsync(w => w.Status == "Open" || w.Status == "In Progress" || w.Status == "On Hold");
var notDispatched = await query.CountAsync(w => (w.Status == "Open") && (w.AssignTo == null || w.AssignTo == ""));
var completed = await query.CountAsync(w => w.Status == "Done");
return Ok(new
{
total,
open,
notDispatched,
completed
});
}
}
}

View file

@ -0,0 +1,349 @@
using Data.SeaHavenIndustries;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SeaHaven.Services.Interfaces;
using SeaHaven.Services.DTOs;
namespace Api.SeaHavenIndustries.Controllers
{
[Authorize]
[ApiController]
[Route("api/[controller]")]
[Route("api/dispatches")]
public class DispatchController : Controller
{
private readonly IDispatchService _dispatchService;
private readonly ApplicationDbContext _db; // Keep for complex queries
public DispatchController(IDispatchService dispatchService, ApplicationDbContext db)
{
_dispatchService = dispatchService;
_db = db;
}
[HttpGet]
public async Task<IActionResult> List(
[FromQuery] string? search = null,
[FromQuery] string? status = null,
[FromQuery] int? vendorId = null,
[FromQuery] DateTime? dateFrom = null,
[FromQuery] DateTime? dateTo = null,
[FromQuery] string? sortBy = "dispatchedAt",
[FromQuery] string? sortDir = "desc",
[FromQuery] int page = 1,
[FromQuery] int pageSize = 25)
{
var q = _db.Dispatches
.Include(d => d.Vendor)
.Include(d => d.WorkOrder).ThenInclude(w => w!.Locations)
.Where(d => d.IsDeleted == null || d.IsDeleted == false);
if (!string.IsNullOrWhiteSpace(search))
{
var s = search.ToLower();
q = q.Where(d =>
(d.DispatchNumber ?? "").ToLower().Contains(s) ||
(d.PONumber ?? "").ToLower().Contains(s) ||
(d.Vendor != null && (d.Vendor.CompanyName ?? "").ToLower().Contains(s)) ||
(d.WorkOrder != null && (d.WorkOrder.InternalWONumber ?? "").ToLower().Contains(s)) ||
(d.WorkOrder != null && (d.WorkOrder.WorkerOrderTitle ?? "").ToLower().Contains(s)) ||
(d.WorkOrder != null && d.WorkOrder.Locations != null && (d.WorkOrder.Locations.Name ?? "").ToLower().Contains(s))
);
}
if (!string.IsNullOrWhiteSpace(status))
q = q.Where(d => d.Status == status);
if (vendorId.HasValue)
q = q.Where(d => d.VendorId == vendorId.Value);
if (dateFrom.HasValue)
q = q.Where(d => d.DispatchedAt >= dateFrom.Value);
if (dateTo.HasValue)
{
var end = dateTo.Value.Date.AddDays(1);
q = q.Where(d => d.DispatchedAt < end);
}
var total = await q.CountAsync();
var dir = (sortDir ?? "desc").ToLowerInvariant() == "asc";
switch ((sortBy ?? "").ToLowerInvariant())
{
case "ponumber":
q = dir ? q.OrderBy(d => d.PONumber) : q.OrderByDescending(d => d.PONumber);
break;
case "dispatchnumber":
q = dir ? q.OrderBy(d => d.DispatchNumber) : q.OrderByDescending(d => d.DispatchNumber);
break;
case "vendor":
q = dir ? q.OrderBy(d => d.Vendor!.CompanyName) : q.OrderByDescending(d => d.Vendor!.CompanyName);
break;
case "nte":
case "nteamount":
q = dir ? q.OrderBy(d => d.NTEAmount) : q.OrderByDescending(d => d.NTEAmount);
break;
case "status":
q = dir ? q.OrderBy(d => d.Status) : q.OrderByDescending(d => d.Status);
break;
case "scheduleddate":
q = dir ? q.OrderBy(d => d.ScheduledDate) : q.OrderByDescending(d => d.ScheduledDate);
break;
case "completeddate":
q = dir ? q.OrderBy(d => d.CompletedDate) : q.OrderByDescending(d => d.CompletedDate);
break;
default:
q = dir ? q.OrderBy(d => d.DispatchedAt) : q.OrderByDescending(d => d.DispatchedAt);
break;
}
var rows = await q
.Skip((Math.Max(page, 1) - 1) * pageSize)
.Take(pageSize)
.Select(d => new
{
d.Id,
d.DispatchNumber,
d.PONumber,
d.NTEAmount,
d.Status,
d.DispatchedAt,
d.ScheduledDate,
d.CompletedDate,
d.AcknowledgedAt,
d.VerifiedAt,
VendorId = d.VendorId,
VendorName = d.Vendor != null ? d.Vendor.CompanyName : null,
WorkOrderId = d.WorkOrderId,
PrimaryWOInternalNumber = d.WorkOrder != null ? d.WorkOrder.InternalWONumber : null,
PrimaryWOTitle = d.WorkOrder != null ? d.WorkOrder.WorkerOrderTitle : null,
LocationName = d.WorkOrder != null && d.WorkOrder.Locations != null ? d.WorkOrder.Locations.Name : null,
WorkOrderCount = _db.DispatchWorkOrders.Count(dwo => dwo.DispatchId == d.Id),
PendingUpliftCount = _db.DispatchUpliftRequests.Count(u => u.DispatchId == d.Id && u.Status == "Pending" && (u.IsDeleted == null || u.IsDeleted == false))
})
.ToListAsync();
return Ok(new DataResponse
{
Status = "Success",
Data = new { total, page, pageSize, items = rows }
});
}
[HttpGet("{id:int}")]
public async Task<IActionResult> GetById(int id)
{
var dispatch = await _db.Dispatches
.Include(d => d.Vendor)
.Include(d => d.WorkOrder).ThenInclude(w => w!.Locations)
.FirstOrDefaultAsync(d => d.Id == id);
if (dispatch == null) return NotFound(new Response { Status = "Error", Message = "Dispatch not found" });
var linkedWOs = await _db.DispatchWorkOrders
.Where(dwo => dwo.DispatchId == id)
.Select(dwo => new
{
dwo.WorkOrderId,
InternalWONumber = dwo.WorkOrder != null ? dwo.WorkOrder.InternalWONumber : null,
Title = dwo.WorkOrder != null ? dwo.WorkOrder.WorkerOrderTitle : null,
Priority = dwo.WorkOrder != null ? dwo.WorkOrder.Priority : null,
Status = dwo.WorkOrder != null ? dwo.WorkOrder.Status : null,
LocationName = dwo.WorkOrder != null && dwo.WorkOrder.Locations != null ? dwo.WorkOrder.Locations.Name : null,
Trade = dwo.WorkOrder != null ? dwo.WorkOrder.Trade : null,
SubTrade = dwo.WorkOrder != null ? dwo.WorkOrder.SubTrade : null,
Problem = dwo.WorkOrder != null ? dwo.WorkOrder.Problem : null
}).ToListAsync();
if (linkedWOs.Count == 0 && dispatch.WorkOrderId.HasValue)
{
var wo = await _db.workOrders
.Include(w => w.Locations)
.FirstOrDefaultAsync(w => w.Id == dispatch.WorkOrderId.Value);
if (wo != null)
{
linkedWOs = new[] { new
{
WorkOrderId = wo.Id,
InternalWONumber = wo.InternalWONumber,
Title = wo.WorkerOrderTitle,
Priority = wo.Priority,
Status = wo.Status,
LocationName = wo.Locations != null ? wo.Locations.Name : null,
Trade = wo.Trade,
SubTrade = wo.SubTrade,
Problem = wo.Problem
}}.ToList();
}
}
var checklist = await _db.DispatchChecklistItems
.Where(c => c.DispatchId == id && (c.IsDeleted == null || c.IsDeleted == false))
.OrderBy(c => c.SortOrder)
.Select(c => new { c.Id, c.ItemText, c.IsCompleted, c.CompletedBy, c.CompletedAt, c.SortOrder, c.WorkOrderId })
.ToListAsync();
var signoffs = await _db.DispatchSignoffs
.Where(s => s.DispatchId == id && (s.IsDeleted == null || s.IsDeleted == false))
.Select(s => new { s.Id, s.SignoffType, s.Name, s.Signature, s.SignatureMethod, s.SignedAt })
.ToListAsync();
var rawComments = await (from c in _db.Comments
where c.DispatchId == id && (c.IsDeleted == null || c.IsDeleted == false)
join u in _db.Users on c.UserId equals u.Id into users
from u in users.DefaultIfEmpty()
orderby c.CreatedDate
select new
{
c.Id,
c.Commenttext,
c.Commenter,
c.CommentType,
c.RecordType,
c.CreatedDate,
c.UserId,
UserFirstName = u != null ? u.FirstName : null,
UserLastName = u != null ? u.LastName : null
})
.ToListAsync();
var comments = rawComments.Select(c => new
{
c.Id,
c.Commenttext,
c.Commenter,
c.CommentType,
c.RecordType,
c.CreatedDate,
UserName = string.Join(" ", new[] { c.UserFirstName, c.UserLastName }
.Where(s => !string.IsNullOrWhiteSpace(s))).Trim()
}).ToList();
var rawUplifts = await (from u in _db.DispatchUpliftRequests
where u.DispatchId == id && (u.IsDeleted == null || u.IsDeleted == false)
join dec in _db.Users on u.DecidedByUserId equals dec.Id into decs
from dec in decs.DefaultIfEmpty()
orderby u.CreatedDate descending
select new
{
u.Id,
u.CurrentNTE,
u.RequestedNTE,
u.VendorReason,
u.Status,
u.RequiredTier,
u.RequestedByVendorName,
RequestedAt = u.CreatedDate,
u.DecidedAt,
u.DecisionNote,
DecidedByFirstName = dec != null ? dec.FirstName : null,
DecidedByLastName = dec != null ? dec.LastName : null
})
.ToListAsync();
var upliftRequests = rawUplifts.Select(u => new
{
u.Id,
u.CurrentNTE,
u.RequestedNTE,
Delta = u.RequestedNTE - (u.CurrentNTE ?? 0m),
u.VendorReason,
u.Status,
u.RequiredTier,
u.RequestedByVendorName,
u.RequestedAt,
u.DecidedAt,
u.DecisionNote,
DecidedByName = string.Join(" ", new[] { u.DecidedByFirstName, u.DecidedByLastName }
.Where(s => !string.IsNullOrWhiteSpace(s))).Trim()
}).ToList();
var dispatchNumber = dispatch.DispatchNumber ?? "";
var workOrderIds = linkedWOs.Select(l => l.WorkOrderId).Where(wid => wid > 0).ToList();
var rawAudit = await (from a in _db.WorkOrderAuditLogs
where workOrderIds.Contains(a.WorkOrderId)
&& a.FieldName != null
&& a.FieldName.Contains(dispatchNumber)
join u in _db.Users on a.UserId equals u.Id into users
from u in users.DefaultIfEmpty()
orderby a.CreatedAt descending
select new
{
a.Id,
a.Action,
a.FieldName,
a.OldValue,
a.NewValue,
a.CreatedAt,
a.UserId,
UserFirstName = u != null ? u.FirstName : null,
UserLastName = u != null ? u.LastName : null
})
.ToListAsync();
var auditLog = rawAudit.Select(a => new
{
Id = a.Id,
Action = a.Action,
FieldName = a.FieldName,
OldValue = a.OldValue,
NewValue = a.NewValue,
CreatedAt = a.CreatedAt,
UserName = string.Join(" ", new[] { a.UserFirstName, a.UserLastName }
.Where(s => !string.IsNullOrWhiteSpace(s))).Trim()
}).ToList();
return Ok(new DataResponse
{
Status = "Success",
Data = new
{
dispatch.Id,
dispatch.DispatchNumber,
dispatch.PONumber,
dispatch.NTEAmount,
dispatch.Description,
dispatch.Status,
dispatch.ScheduledDate,
dispatch.CompletedDate,
dispatch.DispatchedAt,
dispatch.AcknowledgedAt,
dispatch.VerifiedAt,
dispatch.VerifiedBy,
dispatch.EmailSent,
dispatch.WorkOrderId,
Vendor = dispatch.Vendor == null ? null : new
{
dispatch.Vendor.Id,
dispatch.Vendor.CompanyName,
dispatch.Vendor.ContactName,
dispatch.Vendor.Email,
dispatch.Vendor.Phone,
dispatch.Vendor.Address,
dispatch.Vendor.City,
dispatch.Vendor.State,
dispatch.Vendor.Zip,
dispatch.Vendor.TradeSpecialties
},
PrimaryLocation = dispatch.WorkOrder?.Locations == null ? null : new
{
dispatch.WorkOrder.Locations.Id,
dispatch.WorkOrder.Locations.Name,
dispatch.WorkOrder.Locations.Address1,
dispatch.WorkOrder.Locations.City,
dispatch.WorkOrder.Locations.State,
dispatch.WorkOrder.Locations.Zip
},
WorkOrders = linkedWOs,
Checklist = checklist,
Signoffs = signoffs,
Comments = comments,
UpliftRequests = upliftRequests,
AuditLog = auditLog
}
});
}
}
}

View file

@ -0,0 +1,43 @@
using Microsoft.AspNetCore.Mvc;
namespace Api.SeaHavenIndustries.Controllers
{
[ApiController]
[Route("api/dispatch")]
public class DispatchPublicController : Controller
{
[HttpGet("accept/{token}")]
public IActionResult Accept(string token)
{
return Content(BuildHtml(), "text/html");
}
private static string BuildHtml()
{
return @"<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'/>
<meta name='viewport' content='width=device-width, initial-scale=1'/>
<title>Link Expired - Sea Haven Industries</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; background: #f5f5f5; }
.card { background: white; border-radius: 12px; padding: 40px; max-width: 480px; text-align: center; box-shadow: 0 4px 20px rgba(0,0,0,0.1); }
.icon { width: 60px; height: 60px; border-radius: 50%; background: #ffc107; color: white; font-size: 28px; display: flex; align-items: center; justify-content: center; margin: 0 auto 20px; }
h1 { font-size: 22px; color: #333; margin: 0 0 12px; }
p { font-size: 15px; color: #666; line-height: 1.6; margin: 0; }
.footer { margin-top: 30px; font-size: 12px; color: #999; }
</style>
</head>
<body>
<div class='card'>
<div class='icon'>!</div>
<h1>Link No Longer Active</h1>
<p>This dispatch accept link has been retired. Please use the vendor portal link in your most recent dispatch email, or contact your dispatcher for a new link.</p>
<div class='footer'>Sea Haven Industries</div>
</div>
</body>
</html>";
}
}
}

View file

@ -0,0 +1,87 @@
using Data.SeaHavenIndustries;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Api.SeaHavenIndustries.Controllers
{
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class DropdownOptionsController : Controller
{
private readonly ApplicationDbContext _db;
public DropdownOptionsController(ApplicationDbContext db)
{
_db = db;
}
[HttpGet]
public async Task<IActionResult> GetAll([FromQuery] string? category = null)
{
var query = _db.DropdownOptions.Where(d => d.IsActive);
if (!string.IsNullOrWhiteSpace(category))
query = query.Where(d => d.Category == category);
var data = await query.OrderBy(d => d.Category).ThenBy(d => d.SortOrder).ThenBy(d => d.Value).ToListAsync();
return Ok(data);
}
[HttpGet("ByCategory/{category}")]
public async Task<IActionResult> GetByCategory(string category, [FromQuery] string? parent = null)
{
var query = _db.DropdownOptions.Where(d => d.IsActive && d.Category == category);
if (!string.IsNullOrWhiteSpace(parent))
query = query.Where(d => d.ParentValue == parent);
var data = await query.OrderBy(d => d.SortOrder).ThenBy(d => d.Value).ToListAsync();
return Ok(data);
}
[HttpPost]
public async Task<IActionResult> Create([FromBody] DropdownOption model)
{
if (string.IsNullOrWhiteSpace(model.Category) || string.IsNullOrWhiteSpace(model.Value))
return BadRequest(new { message = "Category and Value are required" });
var existing = await _db.DropdownOptions
.AnyAsync(d => d.Category == model.Category && d.Value == model.Value && d.ParentValue == model.ParentValue);
if (existing)
return BadRequest(new { message = "This option already exists" });
model.Id = 0;
model.IsActive = true;
_db.DropdownOptions.Add(model);
await _db.SaveChangesAsync();
return Ok(model);
}
[HttpPut("{id}")]
public async Task<IActionResult> Update(int id, [FromBody] DropdownOption model)
{
var item = await _db.DropdownOptions.FindAsync(id);
if (item == null)
return NotFound(new { message = "Option not found" });
item.Value = model.Value;
item.ParentValue = model.ParentValue;
item.SortOrder = model.SortOrder;
item.IsActive = model.IsActive;
await _db.SaveChangesAsync();
return Ok(item);
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id)
{
var item = await _db.DropdownOptions.FindAsync(id);
if (item == null)
return NotFound(new { message = "Option not found" });
_db.DropdownOptions.Remove(item);
await _db.SaveChangesAsync();
return Ok(new { message = "Deleted" });
}
}
}

View file

@ -2,16 +2,19 @@
using Data.SeaHavenIndustries;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SeaHaven.Services.Interfaces;
using SeaHaven.Services.DTOs;
[ApiController]
[Route("api/Employee")]
[Route("api/employees")]
public class EmployeeController : Controller
{
private readonly ApplicationDbContext _db;
private readonly IEmployeeService _employeeService;
public EmployeeController(ApplicationDbContext db)
public EmployeeController(IEmployeeService employeeService)
{
_db = db;
_employeeService = employeeService;
}
// -----------------------------------
@ -20,42 +23,27 @@ public class EmployeeController : Controller
[HttpPost("AddEmployees")]
public async Task<IActionResult> AddEmployees(AddEmployee_DTO dto)
{
var employee = new Employee
try
{
FirstName = dto.FirstName,
LastName = dto.LastName,
JobTitleId = dto.JobTitleId,
PermissionGroupId = dto.PermissionGroupId,
Status = dto.Status,
LastLogin = dto.LastLogin,
CreatedDate = DateTime.UtcNow
};
if (!string.IsNullOrWhiteSpace(dto.Email))
{
employee.Emails.Add(new EmployeeEmail
var createDto = new CreateEmployeeDTO
{
Address = dto.Email,
IsPrimary = true,
EmailTypeId = 1
});
}
FirstName = dto.FirstName,
LastName = dto.LastName,
JobTitleId = dto.JobTitleId,
DepartmentId = null, // Map from PermissionGroupId if needed
Email = dto.Email,
Phone = dto.Phone
};
if (!string.IsNullOrWhiteSpace(dto.Phone))
var userId = User.Identity?.Name ?? "system";
await _employeeService.CreateEmployeeAsync(createDto, userId);
return Ok(new { Message = "Employee added successfully" });
}
catch (Exception ex)
{
employee.Phones.Add(new EmployeePhone
{
Number = dto.Phone,
Extension = dto.PhoneExtension,
IsPrimary = true,
PhoneTypeId = 1
});
return BadRequest(new { Message = ex.Message });
}
_db.Employees.Add(employee);
await _db.SaveChangesAsync();
return Ok(new { Message = "Employee added successfully" });
}
// -----------------------------------
@ -64,87 +52,54 @@ public class EmployeeController : Controller
[HttpPost("EditEmployees")]
public async Task<IActionResult> EditEmployees(EditEmployee_DTO dto)
{
var employee = await _db.Employees
.Include(e => e.Emails)
.Include(e => e.Phones)
.FirstOrDefaultAsync(e => e.Id == dto.Id);
try
{
var updateDto = new UpdateEmployeeDTO
{
FirstName = dto.FirstName,
LastName = dto.LastName,
JobTitleId = dto.JobTitleId
};
if (employee == null)
return BadRequest("Employee not found");
var userId = User.Identity?.Name ?? "system";
await _employeeService.UpdateEmployeeAsync(dto.Id, updateDto, userId);
employee.FirstName = dto.FirstName;
employee.LastName = dto.LastName;
employee.JobTitleId = dto.JobTitleId;
employee.PermissionGroupId = dto.PermissionGroupId;
employee.Status = dto.Status;
employee.LastLogin = dto.LastLogin;
employee.LastModificationTime = DateTime.UtcNow;
await _db.SaveChangesAsync();
return Ok(new { Message = "Employee updated successfully" });
return Ok(new { Message = "Employee updated successfully" });
}
catch (Exception ex)
{
return BadRequest(new { Message = ex.Message });
}
}
// -----------------------------------
// GET: api/Employee/GetEmployeeList
// -----------------------------------
[HttpGet("GetEmployeeList")]
public IActionResult GetEmployeeList(string? search = "", int page = 1, int pageSize = 12)
public async Task<IActionResult> GetEmployeeList(string? search = "", int page = 1, int pageSize = 12)
{
search ??= "";
var pagedResult = await _employeeService.GetEmployeesPagedAsync(page, pageSize, search);
var totalCount = _db.Employees.Where(e => !e.IsDeleted.HasValue || e.IsDeleted == false).Count();
var data = pagedResult.Items.Select(e => new
{
id = e.Id,
firstName = e.FirstName,
lastName = e.LastName,
jobTitle = e.JobTitleName ?? "",
email = e.Email ?? "",
phone = e.Phone ?? "",
department = e.DepartmentName ?? "",
status = "Active" // Map from employee status if available
});
var data = _db.Employees
.Include(e => e.JobTitle)
.Include(e => e.Department)
.Include(e => e.Emails)
.Include(e => e.Phones)
.Where(e => !e.IsDeleted.HasValue || e.IsDeleted == false)
.Where(e =>
string.IsNullOrWhiteSpace(search) ||
(e.FirstName != null && e.FirstName.ToLower().Contains(search.ToLower())) ||
(e.LastName != null && e.LastName.ToLower().Contains(search.ToLower())) ||
e.Emails.Any(x => x.Address != null && x.Address.ToLower().Contains(search.ToLower())) ||
e.Phones.Any(x => x.Number != null && x.Number.Contains(search))
)
.Select(e => new
{
e.Id,
e.FirstName,
e.LastName,
JobTitleName = e.JobTitle != null ? e.JobTitle.Name : "",
DepartmentName = e.Department != null ? e.Department.Name : "",
e.Status,
PrimaryEmail = e.Emails.Where(x => x.IsPrimary == true).Select(x => x.Address).FirstOrDefault(),
PrimaryPhone = e.Phones.Where(x => x.IsPrimary == true).Select(x => x.Number).FirstOrDefault(),
PhoneExtension = e.Phones.Where(x => x.IsPrimary == true).Select(x => x.Extension).FirstOrDefault()
})
.OrderByDescending(e => e.Id)
.ToList()
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(e => new
{
id = e.Id,
firstName = e.FirstName,
lastName = e.LastName,
jobTitle = e.JobTitleName ?? "",
email = e.PrimaryEmail ?? "",
phone = e.PrimaryPhone != null
? e.PrimaryPhone + (e.PhoneExtension != null ? " ext. " + e.PhoneExtension : "")
: "",
department = e.DepartmentName ?? "",
status = e.Status
});
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
var totalPages = (int)Math.Ceiling(pagedResult.TotalCount / (double)pageSize);
var viewModel = new Pagination_DTO
{
Data = data,
PageNumber = page,
PageSize = pageSize,
TotalCount = totalCount,
TotalCount = pagedResult.TotalCount,
TotalPages = totalPages
};
@ -158,18 +113,8 @@ public class EmployeeController : Controller
{
try
{
var employee = await _db.Employees
.FirstOrDefaultAsync(e => e.Id == id);
if (employee == null)
return BadRequest(new Response
{
Status = "Error",
Message = "Employee not found."
});
_db.Employees.Remove(employee);
await _db.SaveChangesAsync();
var userId = User.Identity?.Name ?? "system";
await _employeeService.DeleteEmployeeAsync(id, userId);
return Ok(new DataResponse
{
@ -177,6 +122,14 @@ public class EmployeeController : Controller
Status = "200"
});
}
catch (InvalidOperationException ex)
{
return BadRequest(new Response
{
Status = "Error",
Message = ex.Message
});
}
catch (Exception ex)
{
return BadRequest(new Response

View file

@ -1,87 +1,164 @@
using Api.SeaHavenIndustries.DTOs;
using System.Security.Claims;
using Api.SeaHavenIndustries.DTOs;
using Data.SeaHavenIndustries;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SeaHaven.Services.Interfaces;
using SeaHaven.Services.DTOs;
using SeaHaven.DataServices.Interfaces;
[ApiController]
[Authorize]
[Route("api/FollowUp")]
[Route("api/followups")]
public class FollowUpController : ControllerBase
{
private readonly ApplicationDbContext _db;
private readonly IFollowUpService _followUpService;
private readonly IFollowUpDataService _followUpDataService; // Keep for direct entity operations
private readonly ApplicationDbContext _db; // Keep for User lookups
private readonly UserManager<ApplicationUser> _users;
public FollowUpController(ApplicationDbContext db)
public FollowUpController(IFollowUpService followUpService, IFollowUpDataService followUpDataService, ApplicationDbContext db, UserManager<ApplicationUser> users)
{
_followUpService = followUpService;
_followUpDataService = followUpDataService;
_db = db;
_users = users;
}
[HttpGet("GetFollowUpList")]
public IActionResult GetFollowUps(
public async Task<IActionResult> GetFollowUps(
string? search = "",
bool completed = false,
string? status = "Open",
int page = 1,
int pageSize = 12)
{
var status = completed ? "Completed" : "Open";
status = string.IsNullOrWhiteSpace(status) ? "Open" : status;
var query = _db.FollowUps
.Include(x => x.Employee)
.Include(x => x.Account)
.Include(x => x.Location)
.Include(x => x.WorkOrder)
.Where(x => x.Status == status);
var pagedResult = await _followUpService.GetFollowUpsPagedAsync(page, pageSize, search, status);
var items = pagedResult.Items;
if (!string.IsNullOrWhiteSpace(search))
var rows = items.Select(x => new
{
search = search.ToLower();
Id = x.Id,
Reason = x.Reason,
ScheduleDate = x.ScheduleDate,
ScheduleStartTime = x.ScheduleStartTime,
EmployeeId = x.EmployeeId,
WorkOrderId = x.WorkOrderId,
AccountId = x.AccountId,
LocationId = x.LocationId,
AccountName = x.AccountName ?? "",
AssignedTo = x.EmployeeName ?? "",
LocationName = x.LocationName ?? "",
WorkOrderNumber = "",
InternalWONumber = "",
Status = x.Status,
CreatedDate = x.CreatedDate,
CreatedById = (string?)null
}).ToList();
query = query.Where(x =>
(x.Reason ?? "").ToLower().Contains(search) ||
(x.Account != null && (x.Account.Name ?? "").ToLower().Contains(search)) ||
(x.Employee != null &&
(
((x.Employee.FirstName ?? "") + " " + (x.Employee.LastName ?? ""))
.ToLower()
.Contains(search)
)
)
);
var creatorIds = rows
.Select(r => r.CreatedById)
.Where(id => !string.IsNullOrEmpty(id))
.Distinct()
.ToList();
var creators = await _db.Users
.Where(u => creatorIds.Contains(u.Id))
.Select(u => new { u.Id, u.FirstName, u.LastName, u.UserName })
.ToDictionaryAsync(u => u.Id, u => u);
string NameFor(string? userId)
{
if (string.IsNullOrEmpty(userId) || !creators.TryGetValue(userId, out var u)) return "";
var full = ((u.FirstName ?? "") + " " + (u.LastName ?? "")).Trim();
return string.IsNullOrWhiteSpace(full) ? (u.UserName ?? "") : full;
}
var totalCount = query.Count();
var data = query
.OrderByDescending(x => x.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(x => new
{
x.Id,
x.Reason,
x.ScheduleDate,
Account = x.Account != null ? x.Account.Name : "",
AssignedTo = x.Employee != null
? (x.Employee.FirstName + " " + x.Employee.LastName)
: "",
LocationName = x.Location != null ? x.Location.Name : "",
WorkOrderNumber = x.WorkOrder != null ? x.WorkOrder.WorkerOrderNumber : "",
x.Status
})
.ToList();
var data = rows.Select(r => new
{
r.Id,
r.Reason,
r.ScheduleDate,
r.ScheduleStartTime,
r.EmployeeId,
r.WorkOrderId,
r.AccountId,
r.LocationId,
r.AccountName,
r.AssignedTo,
r.LocationName,
r.WorkOrderNumber,
r.InternalWONumber,
r.Status,
r.CreatedDate,
r.CreatedById,
CreatedByName = NameFor(r.CreatedById)
}).ToList();
return Ok(new Pagination_DTO
{
Data = data,
PageNumber = page,
PageSize = pageSize,
TotalCount = totalCount,
TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize)
TotalCount = pagedResult.TotalCount,
TotalPages = (int)Math.Ceiling(pagedResult.TotalCount / (double)pageSize)
});
}
[HttpGet("GetById")]
public async Task<IActionResult> GetById(int id)
{
var model = await _followUpDataService.GetByIdWithDetailsAsync(id);
if (model == null) return NotFound();
string createdByName = "";
if (!string.IsNullOrEmpty(model.createdby))
{
var u = await _db.Users
.Where(x => x.Id == model.createdby)
.Select(x => new { x.FirstName, x.LastName, x.UserName })
.FirstOrDefaultAsync();
if (u != null)
{
var n = ((u.FirstName ?? "") + " " + (u.LastName ?? "")).Trim();
createdByName = string.IsNullOrWhiteSpace(n) ? (u.UserName ?? "") : n;
}
}
return Ok(new
{
model.Id,
model.Reason,
model.ScheduleDate,
model.ScheduleStartTime,
model.EmployeeId,
model.WorkOrderId,
model.AccountId,
model.LocationId,
AccountName = model.Account?.Name ?? "",
AssignedTo = model.Employee != null
? (model.Employee.FirstName + " " + model.Employee.LastName)
: "",
LocationName = model.Location?.Name ?? "",
WorkOrderNumber = model.WorkOrder?.WorkerOrderNumber ?? "",
InternalWONumber = model.WorkOrder?.InternalWONumber ?? "",
model.Status,
model.CreatedDate,
CreatedById = model.createdby,
CreatedByName = createdByName
});
}
[HttpPost("Create")]
public async Task<IActionResult> Create(FollowUp_DTO dto)
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var model = new FollowUps
{
Reason = dto.Reason,
@ -92,19 +169,19 @@ public class FollowUpController : ControllerBase
AccountId = dto.AccountId,
LocationId = dto.LocationId,
Status = "Open",
CreatedDate = DateTime.UtcNow
CreatedDate = DateTime.UtcNow,
createdby = userId
};
_db.FollowUps.Add(model);
await _db.SaveChangesAsync();
await _followUpDataService.AddAsync(model);
return Ok(new DataResponse { Status = "200", Message = "Follow Up created" });
return Ok(new DataResponse { Status = "200", Message = "Follow Up created", Data = model.Id });
}
[HttpPost("Update")]
public async Task<IActionResult> Update(FollowUp_DTO dto)
{
var model = await _db.FollowUps.FindAsync(dto.Id);
var model = await _followUpDataService.GetByIdAsync(dto.Id);
if (model == null) return NotFound();
model.Reason = dto.Reason;
@ -116,19 +193,19 @@ public class FollowUpController : ControllerBase
model.LocationId = dto.LocationId;
model.LastModificationTime = DateTime.UtcNow;
await _db.SaveChangesAsync();
await _followUpDataService.UpdateAsync(model);
return Ok(new DataResponse { Status = "200", Message = "Follow Up updated" });
}
[HttpPost("Complete")]
public async Task<IActionResult> Complete(int id)
{
var model = await _db.FollowUps.FindAsync(id);
var model = await _followUpDataService.GetByIdAsync(id);
if (model == null) return NotFound();
model.Status = "Completed";
model.LastModificationTime = DateTime.UtcNow;
await _db.SaveChangesAsync();
await _followUpDataService.UpdateAsync(model);
return Ok();
}
@ -136,13 +213,24 @@ public class FollowUpController : ControllerBase
[HttpPost("Cancel")]
public async Task<IActionResult> Cancel(int id)
{
var model = await _db.FollowUps.FindAsync(id);
var model = await _followUpDataService.GetByIdAsync(id);
if (model == null) return NotFound();
model.Status = "Cancelled";
model.LastModificationTime = DateTime.UtcNow;
await _db.SaveChangesAsync();
await _followUpDataService.UpdateAsync(model);
return Ok();
}
[HttpPost("Delete")]
public async Task<IActionResult> Delete(int id)
{
var model = await _followUpDataService.GetByIdAsync(id);
if (model == null) return NotFound();
await _followUpDataService.DeleteAsync(id);
return Ok(new DataResponse { Status = "200", Message = "Follow Up deleted" });
}
}

View file

@ -0,0 +1,170 @@
using Api.SeaHavenIndustries.DTOs;
using Data.SeaHavenIndustries;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SeaHaven.Services.Interfaces;
using SeaHaven.Services.DTOs;
using SeaHaven.DataServices.Interfaces;
namespace Api.SeaHavenIndustries.Controllers
{
[Authorize]
[ApiController]
[Route("api/[controller]")]
[Route("api/locations")]
public class LocationController : Controller
{
private readonly ILocationService _locationService;
private readonly ILocationDataService _locationDataService; // Keep for direct entity operations
public LocationController(ILocationService locationService, ILocationDataService locationDataService)
{
_locationService = locationService;
_locationDataService = locationDataService;
}
[HttpGet("GetLocationList")]
public async Task<IActionResult> GetLocationList(string? search = "", int page = 1, int pageSize = 10)
{
var pagedResult = await _locationService.GetLocationsPagedAsync(page, pageSize, search);
var data = pagedResult.Items.Select(l => new
{
Id = l.Id,
Name = l.Name,
Title = l.Title,
Address = l.Address1,
Address1 = l.Address1,
Address2 = l.Address2,
City = l.City,
State = l.State,
ZipCode = l.Zip,
Phone = l.PhoneNumber,
Contact = (string?)null,
ContactEmail = l.Email,
Status = l.Status
});
var viewModel = new Pagination_DTO
{
Data = data,
PageNumber = page,
PageSize = pageSize,
TotalCount = pagedResult.TotalCount,
TotalPages = (int)Math.Ceiling(pagedResult.TotalCount / (double)pageSize)
};
return Ok(viewModel);
}
[HttpGet("{id}")]
public async Task<IActionResult> GetLocationById(int id)
{
var location = await _locationDataService.GetByIdAsync(id);
if (location == null)
return NotFound(new Response { Status = "Error", Message = "Location not found" });
var result = new
{
location.Id,
location.Name,
location.Title,
Address = location.Address1,
location.Address1,
location.Address2,
location.City,
location.State,
ZipCode = location.Zip,
Phone = location.PhoneNumber,
Contact = (string?)null,
ContactEmail = location.Email,
location.Status
};
return Ok(result);
}
[HttpPost]
public async Task<IActionResult> AddLocation([FromBody] Location_DTO model)
{
try
{
var location = new Locations
{
Name = model.Name,
Title = model.Title,
Address1 = model.Address,
City = model.City,
State = model.State,
Zip = model.ZipCode,
PhoneNumber = model.Phone,
Email = model.ContactEmail,
Status = model.Status,
CreatedDate = DateTime.Now
};
await _locationDataService.AddAsync(location);
return Ok(new DataResponse { Message = "Location Created Successfully", Status = "200" });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
[HttpPut("{id}")]
public async Task<IActionResult> EditLocation(int id, [FromBody] EditLocation_DTO model)
{
try
{
var exist = await _locationDataService.GetByIdAsync(id);
if (exist == null)
return BadRequest(new Response { Status = "Error", Message = "Location not found" });
exist.Name = model.Name;
exist.Title = model.Title;
exist.Address1 = model.Address;
exist.City = model.City;
exist.State = model.State;
exist.Zip = model.ZipCode; // This is a property, not a method
exist.PhoneNumber = model.Phone;
exist.Email = model.ContactEmail;
exist.Status = model.Status;
exist.LastModificationTime = DateTime.Now;
await _locationDataService.UpdateAsync(exist);
return Ok(new DataResponse { Message = "Location Updated Successfully", Status = "200" });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteLocation(int id)
{
try
{
var exist = await _locationDataService.GetByIdAsync(id);
if (exist == null)
return BadRequest(new Response { Status = "Error", Message = "Location not found" });
await _locationDataService.DeleteAsync(id);
return Ok(new DataResponse { Message = "Location Deleted Successfully", Status = "200" });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
[HttpGet("DeleteLocation")]
public async Task<IActionResult> DeleteLocationByQuery(int id)
{
return await DeleteLocation(id);
}
}
}

View file

@ -2,36 +2,36 @@
using Data.SeaHavenIndustries;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SeaHaven.Services.Interfaces;
using SeaHaven.Services.DTOs;
[ApiController]
[Route("api/PMSchedule")]
[Route("api/pmschedules")]
public class PMScheduleController : ControllerBase
{
private readonly ApplicationDbContext _db;
private readonly IPMScheduleService _pmScheduleService;
public PMScheduleController(ApplicationDbContext db)
public PMScheduleController(IPMScheduleService pmScheduleService)
{
_db = db;
_pmScheduleService = pmScheduleService;
}
[HttpGet("GetList")]
public IActionResult GetList()
public async Task<IActionResult> GetList()
{
var data = _db.PMSchedules
.Include(x => x.Asset)
.Include(x => x.Location)
.OrderByDescending(x => x.Id)
.Select(x => new
{
x.Id,
x.Name,
x.Frequency,
x.StartDate,
x.IsActive,
Asset = x.Asset != null ? x.Asset.Name : null,
Location = x.Location != null ? x.Location.Name : null
})
.ToList();
var items = await _pmScheduleService.GetAllPMSchedulesAsync();
var data = items.Select(x => new
{
Id = x.Id,
Name = x.Name,
Frequency = x.Frequency,
StartDate = x.StartDate,
IsActive = x.IsActive,
Asset = x.AssetName,
Location = x.LocationName
});
return Ok(data);
}
@ -39,31 +39,42 @@ public class PMScheduleController : ControllerBase
[HttpPost("Save")]
public async Task<IActionResult> Save(PMSchedule_DTO dto)
{
PMSchedules model;
if (dto.Id == 0)
try
{
model = new PMSchedules
var userId = User.Identity?.Name ?? "system";
if (dto.Id == 0)
{
CreatedDate = DateTime.UtcNow
};
_db.PMSchedules.Add(model);
var createDto = new CreatePMScheduleDTO
{
Name = dto.Name,
Frequency = dto.Frequency,
StartDate = dto.StartDate,
IsActive = dto.IsActive,
AssetId = dto.AssetId,
LocationId = dto.LocationId
};
await _pmScheduleService.CreatePMScheduleAsync(createDto, userId);
}
else
{
var updateDto = new UpdatePMScheduleDTO
{
Name = dto.Name,
Frequency = dto.Frequency,
StartDate = dto.StartDate,
IsActive = dto.IsActive,
AssetId = dto.AssetId,
LocationId = dto.LocationId
};
await _pmScheduleService.UpdatePMScheduleAsync(dto.Id, updateDto, userId);
}
return Ok();
}
else
catch (Exception ex)
{
model = await _db.PMSchedules.FindAsync(dto.Id);
if (model == null) return NotFound();
model.LastModificationTime = DateTime.UtcNow;
return BadRequest(new { Message = ex.Message });
}
model.Name = dto.Name;
model.Frequency = dto.Frequency;
model.StartDate = dto.StartDate;
model.IsActive = dto.IsActive;
model.AssetId = dto.AssetId;
model.LocationId = dto.LocationId;
await _db.SaveChangesAsync();
return Ok();
}
}

View file

@ -158,13 +158,13 @@ namespace Api.SeaHavenIndustries.Controllers
public IActionResult GetQuotes(string search = "", int page = 1, int pageSize = 10)
{
var totalCount = _db.Quotes.Count();
var data = _db.Quotes.Include(s => s.WorkOrder).AsNoTracking().Where(w => w.QuotId.ToLower().Contains(search.ToLower()) || w.SiteName.ToLower().Contains(search.ToLower()) || w.WorkOrder.WorkerOrderTitle.ToLower().Contains(search.ToLower())).Select(s => new
var data = _db.Quotes.Include(s => s.WorkOrder).AsNoTracking().Where(w => (w.QuotId ?? "").ToLower().Contains(search.ToLower()) || (w.SiteName ?? "").ToLower().Contains(search.ToLower()) || (w.WorkOrder != null && (w.WorkOrder.WorkerOrderTitle ?? "").ToLower().Contains(search.ToLower()))).Select(s => new
{
s.Id,
QuoteId = "QO-" + s.QuotId,
WorkOrder = s.WorkOrder != null ? s.WorkOrder.WorkerOrderNumber : "",
s.ProjectName,
TotalAmount = "$" + s.LineItems.Sum(s => s.Lineitem.Value).ToString(),
TotalAmount = "$" + (s.LineItems != null ? s.LineItems.Sum(l => l.Lineitem ?? 0) : 0).ToString(),
s.SiteName,
s.SiteId,
s.PO,
@ -174,8 +174,8 @@ namespace Api.SeaHavenIndustries.Controllers
s.ContactId,
s.Date,
lineitems = s.LineItems.Select(q => new { q.Descriptions, q.Lineitem, q.LineitemNumber}),
}).OrderByDescending(d => d.Id).ToList().Skip((page - 1) * pageSize)
.Take(pageSize);
}).OrderByDescending(d => d.Id).Skip((page - 1) * pageSize)
.Take(pageSize).ToList();
// Calculate the number of pages
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
@ -201,8 +201,8 @@ namespace Api.SeaHavenIndustries.Controllers
s.Id,
s.PO,
s.TT,
s.Locations.Title,
s.Locations.Name
Title = s.Locations != null ? s.Locations.Title : "",
Name = s.Locations != null ? s.Locations.Name : ""
}).ToListAsync();
return Ok(data);
}
@ -216,7 +216,7 @@ namespace Api.SeaHavenIndustries.Controllers
s.Id,
s.QuotId,
s.Date,
Name = (s.Contacts.FirstName + " " + s.Contacts.LastName).Trim(),
Name = s.Contacts != null ? (s.Contacts.FirstName + " " + s.Contacts.LastName).Trim() : "",
s.PO,
s.TT,
s.SiteName,
@ -225,7 +225,7 @@ namespace Api.SeaHavenIndustries.Controllers
s.BillingAddress,
s.ShippingAddress,
lineitems = s.LineItems.Select(q => new { q.Descriptions, q.Lineitem }),
TotalAmount = "$" + s.LineItems.Sum(s => s.Lineitem.Value).ToString()
TotalAmount = "$" + (s.LineItems != null ? s.LineItems.Sum(l => l.Lineitem ?? 0) : 0).ToString()
}).FirstOrDefaultAsync();
return Ok(data);
}

View file

@ -47,6 +47,8 @@ namespace Api.SeaHavenIndustries.Controllers
try
{
var exist = _db.Categories.Where(w => w.Id == model.Id).FirstOrDefault();
if (exist == null)
return BadRequest(new Response { Status = "Error", Message = "Category not found" });
exist.Name = model.Name;
_db.Categories.Update(exist);
await _db.SaveChangesAsync();
@ -102,13 +104,11 @@ namespace Api.SeaHavenIndustries.Controllers
{
s.Id,
s.Name
}).OrderByDescending(d => d.Id).ToList().Skip((page - 1) * pageSize)
.Take(pageSize);
}).OrderByDescending(d => d.Id).Skip((page - 1) * pageSize)
.Take(pageSize).ToList();
// Calculate the number of pages
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
// Create a view model containing the paged items and pagination metadata
var viewModel = new Pagination_DTO
{
Data = data,
@ -127,8 +127,8 @@ namespace Api.SeaHavenIndustries.Controllers
try
{
var totalCount = _db.workOrders.Where(w=> w.istemplate == true).Count();
var data = _db.workOrders.Where(w => w.istemplate == true).OrderByDescending(d => d.Id).ToList().Skip((page - 1) * pageSize)
.Take(pageSize);
var data = _db.workOrders.Where(w => w.istemplate == true).OrderByDescending(d => d.Id).Skip((page - 1) * pageSize)
.Take(pageSize).ToList();
// Calculate the number of pages
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
@ -157,8 +157,8 @@ namespace Api.SeaHavenIndustries.Controllers
try
{
var totalCount = _db.Users.Count();
var allUsers = _db.Users.Where(w => w.FirstName.ToLower().Contains(search.ToLower())).OrderByDescending(d => d.Id).ToList().Skip((page - 1) * pageSize)
.Take(pageSize);
var allUsers = _db.Users.Where(w => (w.FirstName ?? "").ToLower().Contains((search ?? "").ToLower())).OrderByDescending(d => d.Id).Skip((page - 1) * pageSize)
.Take(pageSize).ToList();
// Calculate the number of pages
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);

View file

@ -0,0 +1,423 @@
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
using Data.SeaHavenIndustries;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Api.SeaHavenIndustries.Controllers
{
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class SyncController : Controller
{
private readonly ApplicationDbContext _db;
private readonly AmazonDynamoDBClient _dynamo;
private const string WO_TABLE = "WorkOrders";
private const string COMMENTS_TABLE = "WorkOrderComments";
private const string VENDOR_REPLIES_TABLE = "VendorReplies";
public SyncController(ApplicationDbContext db)
{
_db = db;
_dynamo = new AmazonDynamoDBClient(Amazon.RegionEndpoint.USEast1);
}
[HttpPost("WorkOrders")]
public async Task<IActionResult> SyncWorkOrders()
{
var synced = 0;
var created = 0;
var updated = 0;
var locationsCreated = 0;
var lastInternalWO = await _db.workOrders
.Where(w => w.InternalWONumber != null && w.InternalWONumber != "")
.OrderByDescending(w => w.InternalWONumber)
.Select(w => w.InternalWONumber)
.FirstOrDefaultAsync();
int nextInternal = 10000001;
if (lastInternalWO != null && int.TryParse(lastInternalWO, out var parsed))
nextInternal = parsed + 1;
Dictionary<string, AttributeValue>? lastKey = null;
do
{
var request = new ScanRequest
{
TableName = WO_TABLE,
Limit = 100,
ExclusiveStartKey = lastKey
};
var response = await _dynamo.ScanAsync(request);
foreach (var item in response.Items)
{
var externalId = GetString(item, "work_order_id");
if (string.IsNullOrEmpty(externalId)) continue;
var existing = await _db.workOrders
.FirstOrDefaultAsync(w => w.ExternalWorkOrderId == externalId);
var locationId = await ResolveLocationId(
GetString(item, "site_code"),
GetString(item, "building"),
GetString(item, "address"));
if (locationId.HasValue && !locationsCreated.Equals(0))
locationsCreated++;
if (existing == null)
{
var wo = new WorkOrder
{
InternalWONumber = (nextInternal++).ToString("D8"),
ExternalWorkOrderId = externalId,
WorkerOrderNumber = externalId,
WorkerOrderTitle = GetString(item, "description"),
Description = GetString(item, "description"),
Status = MapStatus(GetString(item, "wo_status")),
Priority = MapSeverityToPriority(GetString(item, "severity")),
Severity = GetString(item, "severity"),
Customer = GetString(item, "customer"),
SiteCode = GetString(item, "site_code"),
Building = GetString(item, "building"),
LocationId = locationId,
DueDate = ParseDate(GetString(item, "due_date")),
DateReported = ParseDate(GetString(item, "date_reported")),
ScheduledStart = ParseDate(GetString(item, "scheduled_start")),
AssignTo = null,
SourceEmailS3Key = GetString(item, "source_email_s3_key"),
CreatedDate = ParseDate(GetString(item, "created_at")) ?? DateTime.UtcNow,
istemplate = false
};
_db.workOrders.Add(wo);
created++;
}
else
{
existing.WorkerOrderTitle = GetString(item, "description") ?? existing.WorkerOrderTitle;
existing.Description = GetString(item, "description") ?? existing.Description;
existing.Status = MapStatus(GetString(item, "wo_status")) ?? existing.Status;
existing.Priority = MapSeverityToPriority(GetString(item, "severity")) ?? existing.Priority;
existing.Severity = GetString(item, "severity") ?? existing.Severity;
existing.SiteCode = GetString(item, "site_code") ?? existing.SiteCode;
existing.Building = GetString(item, "building") ?? existing.Building;
existing.LocationId = locationId ?? existing.LocationId;
existing.DueDate = ParseDate(GetString(item, "due_date")) ?? existing.DueDate;
existing.DateReported = ParseDate(GetString(item, "date_reported")) ?? existing.DateReported;
existing.ScheduledStart = ParseDate(GetString(item, "scheduled_start")) ?? existing.ScheduledStart;
existing.SourceEmailS3Key = GetString(item, "source_email_s3_key") ?? existing.SourceEmailS3Key;
updated++;
}
synced++;
if (synced % 100 == 0)
await _db.SaveChangesAsync();
}
lastKey = response.LastEvaluatedKey;
} while (lastKey != null && lastKey.Count > 0);
await _db.SaveChangesAsync();
return Ok(new
{
synced,
created,
updated,
locationsCreated = await _db.Locations.CountAsync()
});
}
[HttpPost("Comments")]
public async Task<IActionResult> SyncComments()
{
var synced = 0;
var created = 0;
var skipped = 0;
Dictionary<string, AttributeValue>? lastKey = null;
do
{
var request = new ScanRequest
{
TableName = COMMENTS_TABLE,
Limit = 100,
ExclusiveStartKey = lastKey
};
var response = await _dynamo.ScanAsync(request);
foreach (var item in response.Items)
{
var externalCommentId = GetString(item, "comment_id");
var externalWoId = GetString(item, "work_order_id");
if (string.IsNullOrEmpty(externalCommentId) || string.IsNullOrEmpty(externalWoId))
continue;
var alreadyExists = await _db.Comments
.AnyAsync(c => c.ExternalCommentId == externalCommentId);
if (alreadyExists)
{
skipped++;
synced++;
continue;
}
var workOrder = await _db.workOrders
.FirstOrDefaultAsync(w => w.ExternalWorkOrderId == externalWoId);
if (workOrder == null)
{
skipped++;
continue;
}
var comment = new Comments
{
ExternalCommentId = externalCommentId,
WorkerOrderId = workOrder.Id,
Commenttext = GetString(item, "text"),
Commenter = GetString(item, "commenter"),
RecordType = GetString(item, "record_type"),
CommentType = "customer",
CreatedDate = ParseDate(GetString(item, "created_at")) ?? DateTime.UtcNow,
};
_db.Comments.Add(comment);
created++;
synced++;
if (synced % 100 == 0)
await _db.SaveChangesAsync();
}
lastKey = response.LastEvaluatedKey;
} while (lastKey != null && lastKey.Count > 0);
await _db.SaveChangesAsync();
return Ok(new { synced, created, skipped });
}
[HttpPost("BackfillInternalWONumbers")]
public async Task<IActionResult> BackfillInternalWONumbers()
{
var wosMissing = await _db.workOrders
.Where(w => w.InternalWONumber == null || w.InternalWONumber == "")
.OrderBy(w => w.Id)
.ToListAsync();
if (wosMissing.Count == 0)
return Ok(new { updated = 0 });
var lastWO = await _db.workOrders
.Where(w => w.InternalWONumber != null && w.InternalWONumber != "")
.OrderByDescending(w => w.InternalWONumber)
.Select(w => w.InternalWONumber)
.FirstOrDefaultAsync();
int next = 10000001;
if (lastWO != null && int.TryParse(lastWO, out var lastNum))
next = lastNum + 1;
foreach (var wo in wosMissing)
{
wo.InternalWONumber = next.ToString("D8");
next++;
}
await _db.SaveChangesAsync();
return Ok(new { updated = wosMissing.Count });
}
[HttpPost("BackfillDispatchNumbers")]
public async Task<IActionResult> BackfillDispatchNumbers()
{
var missing = await _db.Dispatches
.Where(d => d.DispatchNumber == null || d.DispatchNumber == "")
.OrderBy(d => d.Id)
.ToListAsync();
if (missing.Count == 0) return Ok(new { updated = 0 });
int next = 1;
var last = await _db.Dispatches
.Where(d => d.DispatchNumber != null && d.DispatchNumber != "")
.OrderByDescending(d => d.DispatchNumber)
.Select(d => d.DispatchNumber)
.FirstOrDefaultAsync();
if (last != null && last.StartsWith("DSP-") && int.TryParse(last.Substring(4), out var n))
next = n + 1;
foreach (var d in missing)
{
d.DispatchNumber = $"DSP-{next:D5}";
next++;
}
await _db.SaveChangesAsync();
return Ok(new { updated = missing.Count });
}
[HttpPost("VendorReplies")]
public async Task<IActionResult> SyncVendorReplies()
{
var synced = 0;
var created = 0;
var skipped = 0;
Dictionary<string, AttributeValue>? lastKey = null;
do
{
var request = new ScanRequest
{
TableName = VENDOR_REPLIES_TABLE,
Limit = 100,
ExclusiveStartKey = lastKey
};
var response = await _dynamo.ScanAsync(request);
foreach (var item in response.Items)
{
var replyId = GetString(item, "reply_id");
var woNumber = GetString(item, "internal_wo_number");
var dispatchNumber = GetString(item, "dispatch_number");
if (string.IsNullOrEmpty(replyId) || string.IsNullOrEmpty(woNumber))
continue;
var alreadyExists = await _db.Comments
.AnyAsync(c => c.ExternalCommentId == replyId);
if (alreadyExists) { skipped++; synced++; continue; }
var workOrder = await _db.workOrders
.FirstOrDefaultAsync(w => w.InternalWONumber == woNumber);
if (workOrder == null) { skipped++; continue; }
int? dispatchId = null;
if (!string.IsNullOrWhiteSpace(dispatchNumber))
{
var dispatch = await _db.Dispatches
.FirstOrDefaultAsync(d => d.DispatchNumber == dispatchNumber && d.WorkOrderId == workOrder.Id);
dispatchId = dispatch?.Id;
}
var comment = new Comments
{
ExternalCommentId = replyId,
WorkerOrderId = workOrder.Id,
DispatchId = dispatchId,
Commenttext = GetString(item, "reply_text"),
Commenter = GetString(item, "sender_email"),
CommentType = "vendor",
RecordType = "vendor_reply",
CreatedDate = ParseDate(GetString(item, "received_at")) ?? DateTime.UtcNow,
};
_db.Comments.Add(comment);
created++;
synced++;
if (synced % 50 == 0)
await _db.SaveChangesAsync();
}
lastKey = response.LastEvaluatedKey;
} while (lastKey != null && lastKey.Count > 0);
await _db.SaveChangesAsync();
return Ok(new { synced, created, skipped });
}
[HttpPost("BackfillCommentTypes")]
public async Task<IActionResult> BackfillCommentTypes()
{
var updated = await _db.Database.ExecuteSqlRawAsync(
"UPDATE Comments SET CommentType = 'customer' WHERE ExternalCommentId IS NOT NULL AND (CommentType IS NULL OR CommentType = '')");
return Ok(new { updated });
}
[HttpPost("All")]
public async Task<IActionResult> SyncAll()
{
var woResult = await SyncWorkOrders() as OkObjectResult;
var commentResult = await SyncComments() as OkObjectResult;
return Ok(new
{
workOrders = woResult?.Value,
comments = commentResult?.Value
});
}
private async Task<int?> ResolveLocationId(string? siteCode, string? building, string? address)
{
if (string.IsNullOrWhiteSpace(siteCode) && string.IsNullOrWhiteSpace(building))
return null;
var matchCode = siteCode ?? building;
var existing = await _db.Locations
.FirstOrDefaultAsync(l => l.Name == matchCode || l.Title == matchCode);
if (existing != null)
return existing.Id;
var location = new Locations
{
Name = matchCode,
Title = building,
Address1 = address,
Status = "Active"
};
_db.Locations.Add(location);
await _db.SaveChangesAsync();
return location.Id;
}
private static string? GetString(Dictionary<string, AttributeValue> item, string key)
{
if (item.TryGetValue(key, out var val) && val.S != null)
return val.S;
return null;
}
private static DateTime? ParseDate(string? value)
{
if (string.IsNullOrWhiteSpace(value)) return null;
if (DateTime.TryParse(value, out var dt)) return dt;
return null;
}
private static string? MapStatus(string? dynamoStatus)
{
return dynamoStatus switch
{
"new" => "Open",
"assigned" => "Open",
"in_progress" => "In Progress",
"on_hold" => "On Hold",
"completed" => "Done",
"cancelled" => "Cancelled",
"unknown" => "Open",
_ => "Open"
};
}
private static string? MapSeverityToPriority(string? severity)
{
if (string.IsNullOrWhiteSpace(severity)) return null;
return $"Sev {severity}";
}
}
}

View file

@ -0,0 +1,146 @@
using Api.SeaHavenIndustries.DTOs;
using Data.SeaHavenIndustries;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Api.SeaHavenIndustries.Controllers
{
[Authorize]
[ApiController]
[Route("api/[controller]")]
[Route("api/tasklisttemplates")]
public class TaskListTemplateController : Controller
{
private readonly ApplicationDbContext _db;
public TaskListTemplateController(ApplicationDbContext db)
{
_db = db;
}
[HttpGet]
public async Task<IActionResult> GetAll()
{
var data = await _db.TaskListTemplates
.Where(t => t.IsActive)
.Include(t => t.Items.OrderBy(i => i.SortOrder))
.OrderBy(t => t.Name)
.Select(t => new
{
t.Id,
t.Name,
t.Description,
t.IsActive,
items = t.Items.OrderBy(i => i.SortOrder).Select(i => new
{
i.Id,
i.ItemText,
i.SortOrder
}).ToList()
})
.ToListAsync();
return Ok(data);
}
[HttpGet("{id}")]
public async Task<IActionResult> GetById(int id)
{
var template = await _db.TaskListTemplates
.Include(t => t.Items.OrderBy(i => i.SortOrder))
.Where(t => t.Id == id)
.Select(t => new
{
t.Id,
t.Name,
t.Description,
t.IsActive,
items = t.Items.OrderBy(i => i.SortOrder).Select(i => new
{
i.Id,
i.ItemText,
i.SortOrder
}).ToList()
})
.FirstOrDefaultAsync();
if (template == null)
return NotFound(new Response { Status = "Error", Message = "Template not found" });
return Ok(template);
}
[HttpPost]
public async Task<IActionResult> Create([FromBody] TaskListTemplate_DTO model)
{
try
{
var template = new TaskListTemplate
{
Name = model.Name,
Description = model.Description,
IsActive = model.IsActive,
Items = model.Items.Select((item, idx) => new TaskListTemplateItem
{
ItemText = item.ItemText,
SortOrder = item.SortOrder > 0 ? item.SortOrder : idx
}).ToList()
};
_db.TaskListTemplates.Add(template);
await _db.SaveChangesAsync();
return Ok(new { message = "Template created", id = template.Id });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
[HttpPut("{id}")]
public async Task<IActionResult> Update(int id, [FromBody] TaskListTemplate_DTO model)
{
try
{
var template = await _db.TaskListTemplates
.Include(t => t.Items)
.FirstOrDefaultAsync(t => t.Id == id);
if (template == null)
return NotFound(new Response { Status = "Error", Message = "Template not found" });
template.Name = model.Name;
template.Description = model.Description;
template.IsActive = model.IsActive;
_db.TaskListTemplateItems.RemoveRange(template.Items);
template.Items = model.Items.Select((item, idx) => new TaskListTemplateItem
{
TaskListTemplateId = id,
ItemText = item.ItemText,
SortOrder = item.SortOrder > 0 ? item.SortOrder : idx
}).ToList();
await _db.SaveChangesAsync();
return Ok(new { message = "Template updated" });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id)
{
var template = await _db.TaskListTemplates.FindAsync(id);
if (template == null)
return NotFound(new Response { Status = "Error", Message = "Template not found" });
template.IsActive = false;
await _db.SaveChangesAsync();
return Ok(new { message = "Template deleted" });
}
}
}

View file

@ -0,0 +1,241 @@
using Data.SeaHavenIndustries;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Security.Claims;
namespace Api.SeaHavenIndustries.Controllers
{
[Authorize]
[ApiController]
[Route("api/[controller]")]
[Route("api/uplifts")]
public class UpliftController : Controller
{
private readonly ApplicationDbContext _db;
private readonly IConfiguration _config;
public UpliftController(ApplicationDbContext db, IConfiguration config)
{
_db = db;
_config = config;
}
[HttpGet]
public async Task<IActionResult> List([FromQuery] string? status = "Pending", [FromQuery] int? tier = null, [FromQuery] int page = 1, [FromQuery] int pageSize = 25)
{
var query = from u in _db.DispatchUpliftRequests
join d in _db.Dispatches on u.DispatchId equals d.Id
join v in _db.Vendors on d.VendorId equals v.Id into vendors
from v in vendors.DefaultIfEmpty()
where (u.IsDeleted == null || u.IsDeleted == false)
select new { u, d, v };
if (!string.IsNullOrWhiteSpace(status))
query = query.Where(x => x.u.Status == status);
if (tier.HasValue)
query = query.Where(x => x.u.RequiredTier == tier.Value);
var total = await query.CountAsync();
var raw = await query
.OrderByDescending(x => x.u.CreatedDate)
.Skip((Math.Max(page, 1) - 1) * pageSize)
.Take(pageSize)
.Select(x => new
{
x.u.Id,
DispatchId = x.d.Id,
x.d.DispatchNumber,
x.d.PONumber,
WorkOrderId = x.d.WorkOrderId,
VendorCompanyName = x.v != null ? x.v.CompanyName : x.u.RequestedByVendorName,
x.u.CurrentNTE,
x.u.RequestedNTE,
x.u.VendorReason,
x.u.RequiredTier,
x.u.Status,
RequestedAt = x.u.CreatedDate,
x.u.DecidedAt,
x.u.DecisionNote
})
.ToListAsync();
var items = raw.Select(r => new
{
r.Id,
r.DispatchId,
r.DispatchNumber,
r.PONumber,
r.WorkOrderId,
r.VendorCompanyName,
r.CurrentNTE,
r.RequestedNTE,
Delta = r.RequestedNTE - (r.CurrentNTE ?? 0m),
r.VendorReason,
r.RequiredTier,
r.Status,
r.RequestedAt,
r.DecidedAt,
r.DecisionNote,
CanDecide = UserCanApprove(r.RequiredTier)
}).ToList();
return Ok(new DataResponse
{
Status = "Success",
Data = new { total, page, pageSize, items }
});
}
[HttpGet("dispatch/{dispatchId:int}")]
public async Task<IActionResult> ListForDispatch(int dispatchId)
{
var rows = await (from u in _db.DispatchUpliftRequests
where u.DispatchId == dispatchId && (u.IsDeleted == null || u.IsDeleted == false)
join dec in _db.Users on u.DecidedByUserId equals dec.Id into decs
from dec in decs.DefaultIfEmpty()
orderby u.CreatedDate descending
select new
{
u.Id,
u.DispatchId,
u.CurrentNTE,
u.RequestedNTE,
Delta = u.RequestedNTE - (u.CurrentNTE ?? 0m),
u.VendorReason,
u.Status,
u.RequiredTier,
u.RequestedByVendorName,
RequestedAt = u.CreatedDate,
u.DecidedAt,
u.DecisionNote,
DecidedByFirstName = dec != null ? dec.FirstName : null,
DecidedByLastName = dec != null ? dec.LastName : null
}).ToListAsync();
var items = rows.Select(r => new
{
r.Id,
r.DispatchId,
r.CurrentNTE,
r.RequestedNTE,
r.Delta,
r.VendorReason,
r.Status,
r.RequiredTier,
r.RequestedByVendorName,
r.RequestedAt,
r.DecidedAt,
r.DecisionNote,
DecidedByName = string.Join(" ", new[] { r.DecidedByFirstName, r.DecidedByLastName }
.Where(s => !string.IsNullOrWhiteSpace(s))).Trim(),
CanDecide = UserCanApprove(r.RequiredTier)
}).ToList();
return Ok(new DataResponse { Status = "Success", Data = items });
}
[HttpPost("{id:int}/approve")]
public async Task<IActionResult> Approve(int id, [FromBody] DecisionRequest? body)
{
var req = await _db.DispatchUpliftRequests.FirstOrDefaultAsync(u => u.Id == id);
if (req == null) return NotFound(new Response { Status = "Error", Message = "Uplift request not found" });
if (req.Status != "Pending") return BadRequest(new Response { Status = "Error", Message = $"Cannot approve a '{req.Status}' request" });
if (!UserCanApprove(req.RequiredTier))
return StatusCode(403, new Response { Status = "Error", Message = $"Approval requires a Tier {req.RequiredTier} role" });
var dispatch = await _db.Dispatches.FirstOrDefaultAsync(d => d.Id == req.DispatchId);
if (dispatch == null) return NotFound(new Response { Status = "Error", Message = "Dispatch not found" });
if (dispatch.Status == "Verified" || dispatch.Status == "Cancelled")
return BadRequest(new Response { Status = "Error", Message = $"Cannot approve uplift on a '{dispatch.Status}' dispatch" });
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var now = DateTime.UtcNow;
var oldNTE = dispatch.NTEAmount ?? 0m;
dispatch.NTEAmount = req.RequestedNTE;
dispatch.LastModificationTime = now;
req.Status = "Approved";
req.DecidedAt = now;
req.DecidedByUserId = userId;
req.DecisionNote = string.IsNullOrWhiteSpace(body?.Note) ? null : body!.Note!.Trim();
req.LastModificationTime = now;
_db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
{
WorkOrderId = dispatch.WorkOrderId ?? 0,
UserId = userId,
FieldName = $"Dispatch {dispatch.DispatchNumber} NTE",
OldValue = $"${oldNTE:F2}",
NewValue = $"${req.RequestedNTE:F2}",
Action = "uplift_approved",
CreatedAt = now
});
await _db.SaveChangesAsync();
return Ok(new DataResponse { Status = "Success", Data = new { req.Id, req.Status, dispatch.NTEAmount } });
}
[HttpPost("{id:int}/deny")]
public async Task<IActionResult> Deny(int id, [FromBody] DecisionRequest body)
{
if (body == null || string.IsNullOrWhiteSpace(body.Note))
return BadRequest(new Response { Status = "Error", Message = "A note is required when denying" });
var req = await _db.DispatchUpliftRequests.FirstOrDefaultAsync(u => u.Id == id);
if (req == null) return NotFound(new Response { Status = "Error", Message = "Uplift request not found" });
if (req.Status != "Pending") return BadRequest(new Response { Status = "Error", Message = $"Cannot deny a '{req.Status}' request" });
if (!UserCanApprove(req.RequiredTier))
return StatusCode(403, new Response { Status = "Error", Message = $"Decision requires a Tier {req.RequiredTier} role" });
var dispatch = await _db.Dispatches.FirstOrDefaultAsync(d => d.Id == req.DispatchId);
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var now = DateTime.UtcNow;
req.Status = "Denied";
req.DecidedAt = now;
req.DecidedByUserId = userId;
req.DecisionNote = body.Note!.Trim();
req.LastModificationTime = now;
_db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
{
WorkOrderId = dispatch?.WorkOrderId ?? 0,
UserId = userId,
FieldName = $"Dispatch {dispatch?.DispatchNumber} Uplift",
OldValue = "Pending",
NewValue = "Denied",
Action = "uplift_denied",
CreatedAt = now
});
await _db.SaveChangesAsync();
return Ok(new DataResponse { Status = "Success", Data = new { req.Id, req.Status } });
}
[HttpGet("can-approve")]
public IActionResult CanApprove([FromQuery] int tier)
{
return Ok(new DataResponse { Status = "Success", Data = new { canApprove = UserCanApprove(tier) } });
}
private bool UserCanApprove(int requiredTier)
{
var key = $"Approvals:Tier{requiredTier}Roles";
var roles = _config.GetSection(key).Get<string[]>() ?? Array.Empty<string>();
foreach (var r in roles)
{
if (!string.IsNullOrWhiteSpace(r) && User.IsInRole(r)) return true;
}
return false;
}
public class DecisionRequest
{
public string? Note { get; set; }
}
}
}

View file

@ -94,7 +94,9 @@ namespace Api.SeaHavenIndustries.Controllers
else
{
var exist1 = await _userManager.Users.FirstAsync(w => w.Id == model.Id);
var exist1 = await _userManager.Users.FirstOrDefaultAsync(w => w.Id == model.Id);
if (exist1 == null)
return BadRequest(new Response { Status = "Error", Message = "User not found" });
var existingRole = await _userManager.GetRolesAsync(exist1);
@ -134,6 +136,8 @@ namespace Api.SeaHavenIndustries.Controllers
try
{
var exist = _db.Users.Where(w => w.Id == user.Id).FirstOrDefault();
if (exist == null)
return BadRequest(new Response { Status = "Error", Message = "User not found" });
exist.FirstName = user.Name;
exist.EmailConfirmed = true;
exist.UserName = user.Email;

View file

@ -0,0 +1,205 @@
using Api.SeaHavenIndustries.DTOs;
using Data.SeaHavenIndustries;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SeaHaven.Services.Interfaces;
using SeaHaven.Services.DTOs;
namespace Api.SeaHavenIndustries.Controllers
{
[Authorize]
[ApiController]
[Route("api/[controller]")]
[Route("api/vendors")]
public class VendorController : Controller
{
private readonly IVendorService _vendorService;
private readonly ApplicationDbContext _db; // Keep for complex queries like nearby vendors
private readonly Helper.ZipCodeDistance _zipDistance;
private readonly Helper.VendorPortalTokenService _vendorTokens;
private readonly IConfiguration _config;
public VendorController(IVendorService vendorService, ApplicationDbContext db, Helper.ZipCodeDistance zipDistance, Helper.VendorPortalTokenService vendorTokens, IConfiguration config)
{
_vendorService = vendorService;
_db = db;
_zipDistance = zipDistance;
_vendorTokens = vendorTokens;
_config = config;
}
[HttpGet("GetVendorList")]
public async Task<IActionResult> GetVendorList(string? search = "", int page = 1, int pageSize = 10)
{
var pagedResult = await _vendorService.GetVendorsPagedAsync(page, pageSize, search);
var data = pagedResult.Items.Select(v => new
{
v.Id,
CompanyName = v.Name,
v.Email,
v.Phone
});
return Ok(new Pagination_DTO
{
Data = data,
PageNumber = page,
PageSize = pageSize,
TotalCount = pagedResult.TotalCount,
TotalPages = (int)Math.Ceiling(pagedResult.TotalCount / (double)pageSize)
});
}
[HttpGet("{id}")]
public async Task<IActionResult> GetVendorById(int id)
{
var vendor = await _vendorService.GetVendorByIdAsync(id);
if (vendor == null)
return NotFound(new Response { Status = "Error", Message = "Vendor not found" });
return Ok(vendor);
}
[HttpPost]
public async Task<IActionResult> Create([FromBody] Vendor_DTO model)
{
try
{
var createDto = new CreateVendorDTO
{
Name = model.CompanyName ?? throw new ArgumentException("CompanyName is required")
};
var userId = User.Identity?.Name ?? "system";
await _vendorService.CreateVendorAsync(createDto, userId);
return Ok(new DataResponse { Message = "Vendor Created", Status = "200" });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
[HttpPut("{id}")]
public async Task<IActionResult> Update(int id, [FromBody] EditVendor_DTO model)
{
try
{
var updateDto = new UpdateVendorDTO
{
Name = model.CompanyName
};
var userId = User.Identity?.Name ?? "system";
await _vendorService.UpdateVendorAsync(id, updateDto, userId);
return Ok(new DataResponse { Message = "Vendor Updated", Status = "200" });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id)
{
var userId = User.Identity?.Name ?? "system";
await _vendorService.DeleteVendorAsync(id, userId);
return Ok(new DataResponse { Message = "Vendor Deleted", Status = "200" });
}
[HttpGet("Dropdown")]
public async Task<IActionResult> GetDropdown([FromQuery] string? trade = null, [FromQuery] string? siteZip = null)
{
var query = _db.Vendors.Where(v => v.IsActive);
var vendors = await query.OrderBy(v => v.CompanyName)
.Select(v => new
{
v.Id,
v.CompanyName,
v.TradeSpecialties,
v.Address,
v.City,
v.State,
v.Zip
})
.ToListAsync();
var result = vendors.Select(v =>
{
var distance = _zipDistance.GetDistanceMiles(siteZip, v.Zip);
var addr = new[] { v.Address, v.City, v.State, v.Zip }
.Where(s => !string.IsNullOrWhiteSpace(s));
return new
{
v.Id,
v.CompanyName,
v.TradeSpecialties,
address = string.Join(", ", addr),
distanceMiles = distance.HasValue ? Math.Round(distance.Value, 1) : (double?)null
};
}).ToList();
if (!string.IsNullOrWhiteSpace(trade))
{
var tradeMatched = result.Where(v => (v.TradeSpecialties ?? "").Contains(trade)).ToList();
var rest = result.Where(v => !(v.TradeSpecialties ?? "").Contains(trade)).ToList();
result = tradeMatched.Concat(rest).ToList();
}
if (!string.IsNullOrWhiteSpace(siteZip))
{
result = result.OrderBy(v => v.distanceMiles ?? 99999).ToList();
}
return Ok(result);
}
[HttpGet("{id:int}/portal-token")]
public async Task<IActionResult> GetPortalToken(int id)
{
var vendor = await _db.Vendors.FindAsync(id);
if (vendor == null) return NotFound();
var token = await _vendorTokens.GetOrCreateActiveTokenAsync(id);
return Ok(BuildPortalTokenResponse(token));
}
[HttpPost("{id:int}/portal-token/rotate")]
public async Task<IActionResult> RotatePortalToken(int id)
{
var vendor = await _db.Vendors.FindAsync(id);
if (vendor == null) return NotFound();
var token = await _vendorTokens.RotateAsync(id);
return Ok(BuildPortalTokenResponse(token));
}
[HttpPost("{id:int}/portal-token/revoke")]
public async Task<IActionResult> RevokePortalToken(int id)
{
var vendor = await _db.Vendors.FindAsync(id);
if (vendor == null) return NotFound();
await _vendorTokens.RevokeAllAsync(id);
return Ok(new { revoked = true });
}
private object BuildPortalTokenResponse(VendorAccessToken token)
{
var frontendBase = _config.GetValue<string>("FrontendBaseUrl")?.TrimEnd('/') ?? "";
return new
{
token.Token,
token.IssuedAt,
token.ExpiresAt,
token.LastUsedAt,
PortalUrl = $"{frontendBase}/v/{token.Token}/dashboard"
};
}
}
}

View file

@ -0,0 +1,696 @@
using Api.SeaHavenIndustries.Helper;
using Data.SeaHavenIndustries;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
namespace Api.SeaHavenIndustries.Controllers
{
[ApiController]
[Route("api/vendor-portal")]
[Route("api/vendorportal")]
public class VendorPortalController : Controller
{
private const string TokenHeader = "X-Vendor-Token";
private static readonly string[] VendorViewableStatuses =
new[] { "Sent", "Acknowledged", "In Progress", "Completed", "Verified", "Cancelled" };
private readonly ApplicationDbContext _db;
private readonly VendorPortalTokenService _tokens;
private readonly IConfiguration _config;
private readonly SendMessage _sendMessage;
public VendorPortalController(ApplicationDbContext db, VendorPortalTokenService tokens, IConfiguration config, SendMessage sendMessage)
{
_db = db;
_tokens = tokens;
_config = config;
_sendMessage = sendMessage;
}
[HttpGet("session")]
public async Task<IActionResult> Session()
{
var vendor = await ResolveVendorAsync();
if (vendor == null) return Unauthorized(new Response { Status = "Error", Message = "Invalid or expired token" });
return Ok(new DataResponse
{
Status = "Success",
Data = new
{
vendor.Id,
vendor.CompanyName,
vendor.ContactName,
vendor.Email,
vendor.Phone
}
});
}
[HttpGet("dispatches")]
public async Task<IActionResult> ListDispatches([FromQuery] string? status = null)
{
var vendor = await ResolveVendorAsync();
if (vendor == null) return Unauthorized(new Response { Status = "Error", Message = "Invalid or expired token" });
var query = _db.Dispatches
.Include(d => d.WorkOrder).ThenInclude(w => w!.Locations)
.Where(d => d.VendorId == vendor.Id && (d.IsDeleted == null || d.IsDeleted == false));
if (!string.IsNullOrWhiteSpace(status))
{
query = query.Where(d => d.Status == status);
}
var dispatches = await query
.OrderByDescending(d => d.DispatchedAt ?? d.CreatedDate)
.Select(d => new
{
d.Id,
d.DispatchNumber,
d.PONumber,
d.Status,
d.NTEAmount,
d.ScheduledDate,
d.CompletedDate,
d.DispatchedAt,
d.AcknowledgedAt,
WorkOrderTitle = d.WorkOrder!.WorkerOrderTitle,
InternalWONumber = d.WorkOrder!.InternalWONumber,
LocationName = d.WorkOrder!.Locations != null ? d.WorkOrder!.Locations.Name : null,
LocationCity = d.WorkOrder!.Locations != null ? d.WorkOrder!.Locations.City : null,
LocationState = d.WorkOrder!.Locations != null ? d.WorkOrder!.Locations.State : null
})
.ToListAsync();
return Ok(new DataResponse { Status = "Success", Data = dispatches });
}
[HttpGet("dispatches/{id:int}")]
public async Task<IActionResult> GetDispatch(int id)
{
var vendor = await ResolveVendorAsync();
if (vendor == null) return Unauthorized(new Response { Status = "Error", Message = "Invalid or expired token" });
var dispatch = await LoadVendorDispatchAsync(id, vendor.Id);
if (dispatch == null) return NotFound(new Response { Status = "Error", Message = "Dispatch not found" });
var checklist = await _db.DispatchChecklistItems
.Where(c => c.DispatchId == id && (c.IsDeleted == null || c.IsDeleted == false))
.OrderBy(c => c.SortOrder)
.Select(c => new { c.Id, c.ItemText, c.IsCompleted, c.CompletedBy, c.CompletedAt })
.ToListAsync();
var signoffs = await _db.DispatchSignoffs
.Where(s => s.DispatchId == id && (s.IsDeleted == null || s.IsDeleted == false))
.Select(s => new { s.Id, s.SignoffType, s.Name, s.SignatureMethod, s.SignedAt })
.ToListAsync();
var rawUplifts = await (from u in _db.DispatchUpliftRequests
where u.DispatchId == id && (u.IsDeleted == null || u.IsDeleted == false)
join dec in _db.Users on u.DecidedByUserId equals dec.Id into decs
from dec in decs.DefaultIfEmpty()
orderby u.CreatedDate descending
select new
{
u.Id,
u.CurrentNTE,
u.RequestedNTE,
u.VendorReason,
u.Status,
u.RequiredTier,
u.RequestedByVendorName,
u.CreatedDate,
u.DecidedAt,
u.DecisionNote,
DecidedByFirstName = dec != null ? dec.FirstName : null,
DecidedByLastName = dec != null ? dec.LastName : null
})
.ToListAsync();
var upliftRequests = rawUplifts.Select(u => new
{
u.Id,
u.CurrentNTE,
u.RequestedNTE,
u.VendorReason,
u.Status,
u.RequiredTier,
u.RequestedByVendorName,
RequestedAt = u.CreatedDate,
u.DecidedAt,
u.DecisionNote,
DecidedByName = string.Join(" ", new[] { u.DecidedByFirstName, u.DecidedByLastName }
.Where(s => !string.IsNullOrWhiteSpace(s))).Trim()
}).ToList();
var rawComments = await (from c in _db.Comments
where c.DispatchId == id
&& c.CommentType != "internal"
&& (c.IsDeleted == null || c.IsDeleted == false)
join u in _db.Users on c.UserId equals u.Id into users
from u in users.DefaultIfEmpty()
orderby c.CreatedDate
select new
{
c.Id,
c.Commenttext,
c.Commenter,
c.CommentType,
c.CreatedDate,
c.UserId,
UserFirstName = u != null ? u.FirstName : null,
UserLastName = u != null ? u.LastName : null
})
.ToListAsync();
var comments = rawComments.Select(c =>
{
var hasShocUser = !string.IsNullOrWhiteSpace(c.UserId);
var userName = string.Join(" ", new[] { c.UserFirstName, c.UserLastName }
.Where(s => !string.IsNullOrWhiteSpace(s))).Trim();
string resolvedType;
string resolvedCommenter;
if (hasShocUser)
{
resolvedType = "dispatcher";
resolvedCommenter = !string.IsNullOrWhiteSpace(userName) ? userName
: !string.IsNullOrWhiteSpace(c.Commenter) ? c.Commenter!
: "Dispatcher";
}
else if (c.CommentType == "customer")
{
resolvedType = "customer";
resolvedCommenter = !string.IsNullOrWhiteSpace(c.Commenter) ? c.Commenter! : "Customer";
}
else
{
resolvedType = "vendor";
resolvedCommenter = !string.IsNullOrWhiteSpace(c.Commenter) ? c.Commenter! : vendor.CompanyName ?? "Vendor";
}
return new
{
c.Id,
c.Commenttext,
Commenter = resolvedCommenter,
CommentType = resolvedType,
c.CreatedDate
};
}).ToList();
return Ok(new DataResponse
{
Status = "Success",
Data = new
{
dispatch.Id,
dispatch.DispatchNumber,
dispatch.PONumber,
dispatch.Status,
dispatch.NTEAmount,
dispatch.Description,
dispatch.ScheduledDate,
dispatch.CompletedDate,
dispatch.DispatchedAt,
dispatch.AcknowledgedAt,
WorkOrder = dispatch.WorkOrder == null ? null : new
{
dispatch.WorkOrder.Id,
dispatch.WorkOrder.InternalWONumber,
dispatch.WorkOrder.WorkerOrderTitle,
dispatch.WorkOrder.Description,
dispatch.WorkOrder.Priority,
dispatch.WorkOrder.DueDate,
dispatch.WorkOrder.Trade,
dispatch.WorkOrder.SubTrade,
dispatch.WorkOrder.Problem
},
Location = dispatch.WorkOrder?.Locations == null ? null : new
{
dispatch.WorkOrder.Locations.Name,
dispatch.WorkOrder.Locations.Address1,
dispatch.WorkOrder.Locations.City,
dispatch.WorkOrder.Locations.State,
dispatch.WorkOrder.Locations.Zip
},
Checklist = checklist,
Signoffs = signoffs,
Comments = comments,
UpliftRequests = upliftRequests
}
});
}
[HttpPost("dispatches/{id:int}/accept")]
public async Task<IActionResult> Accept(int id)
{
var vendor = await ResolveVendorAsync();
if (vendor == null) return Unauthorized(new Response { Status = "Error", Message = "Invalid or expired token" });
var dispatch = await LoadVendorDispatchAsync(id, vendor.Id);
if (dispatch == null) return NotFound(new Response { Status = "Error", Message = "Dispatch not found" });
if (dispatch.Status != "Sent")
{
return BadRequest(new Response { Status = "Error", Message = $"Cannot accept a dispatch with status '{dispatch.Status}'" });
}
var now = DateTime.UtcNow;
dispatch.Status = "Acknowledged";
dispatch.AcknowledgedAt = now;
dispatch.LastModificationTime = now;
_db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
{
WorkOrderId = dispatch.WorkOrderId ?? 0,
FieldName = $"Dispatch {dispatch.DispatchNumber} Status",
OldValue = "Sent",
NewValue = "Acknowledged",
Action = "vendor_accept",
CreatedAt = now
});
await _db.SaveChangesAsync();
return Ok(new DataResponse { Status = "Success", Data = new { dispatch.Id, dispatch.Status, dispatch.AcknowledgedAt } });
}
[HttpPost("dispatches/{id:int}/status")]
public async Task<IActionResult> ChangeStatus(int id, [FromBody] ChangeStatusRequest body)
{
var vendor = await ResolveVendorAsync();
if (vendor == null) return Unauthorized(new Response { Status = "Error", Message = "Invalid or expired token" });
var dispatch = await LoadVendorDispatchAsync(id, vendor.Id);
if (dispatch == null) return NotFound(new Response { Status = "Error", Message = "Dispatch not found" });
var from = dispatch.Status;
var to = body.Status;
if (!IsAllowedVendorTransition(from, to))
{
return BadRequest(new Response { Status = "Error", Message = $"Transition from '{from}' to '{to}' is not allowed" });
}
var now = DateTime.UtcNow;
dispatch.Status = to;
dispatch.LastModificationTime = now;
if (to == "Completed") dispatch.CompletedDate = now;
_db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
{
WorkOrderId = dispatch.WorkOrderId ?? 0,
FieldName = $"Dispatch {dispatch.DispatchNumber} Status",
OldValue = from,
NewValue = to,
Action = "vendor_status_change",
CreatedAt = now
});
await _db.SaveChangesAsync();
return Ok(new DataResponse { Status = "Success", Data = new { dispatch.Id, dispatch.Status, dispatch.CompletedDate } });
}
[HttpPost("dispatches/{id:int}/request-cancel")]
public async Task<IActionResult> RequestCancel(int id, [FromBody] RequestCancelRequest body)
{
var vendor = await ResolveVendorAsync();
if (vendor == null) return Unauthorized(new Response { Status = "Error", Message = "Invalid or expired token" });
var dispatch = await LoadVendorDispatchAsync(id, vendor.Id);
if (dispatch == null) return NotFound(new Response { Status = "Error", Message = "Dispatch not found" });
if (dispatch.Status == "Verified" || dispatch.Status == "Cancelled")
{
return BadRequest(new Response { Status = "Error", Message = $"Cannot request cancel on a '{dispatch.Status}' dispatch" });
}
var now = DateTime.UtcNow;
var reason = string.IsNullOrWhiteSpace(body?.Reason) ? "(no reason provided)" : body!.Reason!.Trim();
_db.Comments.Add(new Comments
{
DispatchId = id,
WorkerOrderId = dispatch.WorkOrderId,
Commenter = vendor.CompanyName,
CommentType = "vendor",
RecordType = "cancel_request",
Commenttext = $"Vendor requested cancellation: {reason}",
CreatedDate = now
});
_db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
{
WorkOrderId = dispatch.WorkOrderId ?? 0,
FieldName = $"Dispatch {dispatch.DispatchNumber}",
OldValue = dispatch.Status,
NewValue = "Cancel Requested",
Action = "vendor_request_cancel",
CreatedAt = now
});
await _db.SaveChangesAsync();
return Ok(new DataResponse { Status = "Success", Message = "Cancel request sent to dispatcher" });
}
[HttpPost("dispatches/{id:int}/checklist/{itemId:int}")]
public async Task<IActionResult> UpdateChecklistItem(int id, int itemId, [FromBody] ChecklistUpdateRequest body)
{
var vendor = await ResolveVendorAsync();
if (vendor == null) return Unauthorized(new Response { Status = "Error", Message = "Invalid or expired token" });
var dispatch = await LoadVendorDispatchAsync(id, vendor.Id);
if (dispatch == null) return NotFound(new Response { Status = "Error", Message = "Dispatch not found" });
if (dispatch.Status == "Verified" || dispatch.Status == "Cancelled")
{
return BadRequest(new Response { Status = "Error", Message = "Dispatch is locked" });
}
var item = await _db.DispatchChecklistItems
.FirstOrDefaultAsync(c => c.Id == itemId && c.DispatchId == id);
if (item == null) return NotFound(new Response { Status = "Error", Message = "Checklist item not found" });
var now = DateTime.UtcNow;
item.IsCompleted = body.IsCompleted;
item.CompletedBy = body.IsCompleted ? vendor.CompanyName : null;
item.CompletedAt = body.IsCompleted ? now : null;
item.LastModificationTime = now;
await _db.SaveChangesAsync();
return Ok(new DataResponse
{
Status = "Success",
Data = new { item.Id, item.IsCompleted, item.CompletedBy, item.CompletedAt }
});
}
[HttpPost("dispatches/{id:int}/signoff")]
public async Task<IActionResult> AddSignoff(int id, [FromBody] SignoffRequest body)
{
var vendor = await ResolveVendorAsync();
if (vendor == null) return Unauthorized(new Response { Status = "Error", Message = "Invalid or expired token" });
var dispatch = await LoadVendorDispatchAsync(id, vendor.Id);
if (dispatch == null) return NotFound(new Response { Status = "Error", Message = "Dispatch not found" });
if (dispatch.Status != "In Progress" && dispatch.Status != "Completed")
{
return BadRequest(new Response { Status = "Error", Message = "Signoffs only allowed on In Progress or Completed dispatches" });
}
if (string.IsNullOrWhiteSpace(body?.Name) || string.IsNullOrWhiteSpace(body?.Signature))
{
return BadRequest(new Response { Status = "Error", Message = "Name and signature are required" });
}
var signoffType = (body?.SignoffType ?? "vendor").Trim().ToLowerInvariant();
if (signoffType != "vendor" && signoffType != "customer")
{
return BadRequest(new Response { Status = "Error", Message = "signoffType must be 'vendor' or 'customer'" });
}
var existing = await _db.DispatchSignoffs
.FirstOrDefaultAsync(s => s.DispatchId == id && s.SignoffType == signoffType);
if (existing != null)
{
return BadRequest(new Response { Status = "Error", Message = $"A {signoffType} signoff already exists for this dispatch" });
}
var now = DateTime.UtcNow;
var signoff = new DispatchSignoff
{
DispatchId = id,
SignoffType = signoffType,
Name = body!.Name,
Signature = body.Signature,
SignatureMethod = body.SignatureMethod ?? "drawn",
SignedAt = now,
CreatedDate = now
};
_db.DispatchSignoffs.Add(signoff);
_db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
{
WorkOrderId = dispatch.WorkOrderId ?? 0,
FieldName = $"Dispatch {dispatch.DispatchNumber} Signoff",
OldValue = null,
NewValue = $"{(signoffType == "customer" ? "Customer" : "Vendor")}: {body.Name}",
Action = signoffType == "customer" ? "customer_signoff" : "vendor_signoff",
CreatedAt = now
});
await _db.SaveChangesAsync();
return Ok(new DataResponse { Status = "Success", Data = new { signoff.Id, signoff.SignedAt } });
}
[HttpPost("dispatches/{id:int}/comments")]
public async Task<IActionResult> AddComment(int id, [FromBody] CommentRequest body)
{
var vendor = await ResolveVendorAsync();
if (vendor == null) return Unauthorized(new Response { Status = "Error", Message = "Invalid or expired token" });
var dispatch = await LoadVendorDispatchAsync(id, vendor.Id);
if (dispatch == null) return NotFound(new Response { Status = "Error", Message = "Dispatch not found" });
if (string.IsNullOrWhiteSpace(body?.CommentText))
{
return BadRequest(new Response { Status = "Error", Message = "Comment cannot be empty" });
}
var now = DateTime.UtcNow;
var comment = new Comments
{
DispatchId = id,
WorkerOrderId = dispatch.WorkOrderId,
Commenter = vendor.CompanyName,
CommentType = "vendor",
RecordType = "comment",
Commenttext = body!.CommentText,
CreatedDate = now
};
_db.Comments.Add(comment);
await _db.SaveChangesAsync();
return Ok(new DataResponse
{
Status = "Success",
Data = new { comment.Id, comment.Commenttext, comment.Commenter, comment.CreatedDate }
});
}
[HttpPost("dispatches/{id:int}/uplift-request")]
public async Task<IActionResult> RequestUplift(int id, [FromBody] UpliftRequestBody body)
{
var vendor = await ResolveVendorAsync();
if (vendor == null) return Unauthorized(new Response { Status = "Error", Message = "Invalid or expired token" });
var dispatch = await LoadVendorDispatchAsync(id, vendor.Id);
if (dispatch == null) return NotFound(new Response { Status = "Error", Message = "Dispatch not found" });
if (dispatch.Status == "Verified" || dispatch.Status == "Cancelled")
{
return BadRequest(new Response { Status = "Error", Message = $"Cannot request uplift on a '{dispatch.Status}' dispatch" });
}
if (body == null || body.RequestedNTE <= 0)
{
return BadRequest(new Response { Status = "Error", Message = "Requested NTE must be greater than zero" });
}
var current = dispatch.NTEAmount ?? 0m;
if (body.RequestedNTE <= current)
{
return BadRequest(new Response { Status = "Error", Message = "Requested NTE must be greater than the current NTE" });
}
var pendingExists = await _db.DispatchUpliftRequests
.AnyAsync(u => u.DispatchId == id && u.Status == "Pending" && (u.IsDeleted == null || u.IsDeleted == false));
if (pendingExists)
{
return BadRequest(new Response { Status = "Error", Message = "A pending uplift request already exists for this dispatch" });
}
var tier1Max = _config.GetValue<decimal?>("Approvals:UpliftTier1MaxUsd") ?? 2500m;
var delta = body.RequestedNTE - current;
var requiredTier = delta > tier1Max ? 2 : 1;
var now = DateTime.UtcNow;
var req = new DispatchUpliftRequest
{
DispatchId = id,
CurrentNTE = current,
RequestedNTE = body.RequestedNTE,
VendorReason = string.IsNullOrWhiteSpace(body.Reason) ? null : body.Reason!.Trim(),
Status = "Pending",
RequiredTier = requiredTier,
RequestedByVendorName = vendor.CompanyName,
CreatedDate = now
};
_db.DispatchUpliftRequests.Add(req);
_db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
{
WorkOrderId = dispatch.WorkOrderId ?? 0,
FieldName = $"Dispatch {dispatch.DispatchNumber} Uplift",
OldValue = $"${current:F2}",
NewValue = $"${body.RequestedNTE:F2}",
Action = "uplift_requested",
CreatedAt = now
});
await _db.SaveChangesAsync();
await NotifyDispatcherOfUpliftAsync(dispatch, req, vendor);
return Ok(new DataResponse
{
Status = "Success",
Data = new { req.Id, req.Status, req.RequiredTier, req.CurrentNTE, req.RequestedNTE }
});
}
[HttpPost("dispatches/{id:int}/uplift-request/{requestId:int}/cancel")]
public async Task<IActionResult> CancelUpliftRequest(int id, int requestId)
{
var vendor = await ResolveVendorAsync();
if (vendor == null) return Unauthorized(new Response { Status = "Error", Message = "Invalid or expired token" });
var dispatch = await LoadVendorDispatchAsync(id, vendor.Id);
if (dispatch == null) return NotFound(new Response { Status = "Error", Message = "Dispatch not found" });
var req = await _db.DispatchUpliftRequests
.FirstOrDefaultAsync(u => u.Id == requestId && u.DispatchId == id);
if (req == null) return NotFound(new Response { Status = "Error", Message = "Uplift request not found" });
if (req.Status != "Pending")
{
return BadRequest(new Response { Status = "Error", Message = $"Cannot cancel a '{req.Status}' uplift request" });
}
var now = DateTime.UtcNow;
req.Status = "Cancelled";
req.DecidedAt = now;
req.LastModificationTime = now;
_db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
{
WorkOrderId = dispatch.WorkOrderId ?? 0,
FieldName = $"Dispatch {dispatch.DispatchNumber} Uplift",
OldValue = "Pending",
NewValue = "Cancelled",
Action = "uplift_cancelled",
CreatedAt = now
});
await _db.SaveChangesAsync();
return Ok(new DataResponse { Status = "Success", Data = new { req.Id, req.Status } });
}
private async Task NotifyDispatcherOfUpliftAsync(Dispatch dispatch, DispatchUpliftRequest req, Vendor vendor)
{
try
{
var dispatchAudit = await _db.WorkOrderAuditLogs
.Where(a => a.WorkOrderId == (dispatch.WorkOrderId ?? 0) && a.Action == "dispatch" && !string.IsNullOrEmpty(a.UserId))
.OrderByDescending(a => a.CreatedAt)
.FirstOrDefaultAsync();
if (dispatchAudit == null || string.IsNullOrWhiteSpace(dispatchAudit.UserId)) return;
var dispatcher = await _db.Users.FindAsync(dispatchAudit.UserId);
if (dispatcher == null || string.IsNullOrWhiteSpace(dispatcher.Email)) return;
var frontendBase = _config.GetValue<string>("FrontendBaseUrl")?.TrimEnd('/') ?? "";
var workOrderLink = dispatch.WorkOrderId.HasValue
? $"{frontendBase}/workorders/{dispatch.WorkOrderId.Value}"
: frontendBase;
var vendorName = System.Net.WebUtility.HtmlEncode(vendor.CompanyName ?? "Vendor");
var reason = System.Net.WebUtility.HtmlEncode(req.VendorReason ?? "(no reason provided)");
var dispatchNum = System.Net.WebUtility.HtmlEncode(dispatch.DispatchNumber ?? "");
var tierText = req.RequiredTier == 2 ? "Tier 2 (Manager approval required)" : "Tier 1";
var subject = $"[Uplift Request] {dispatch.DispatchNumber} — {vendor.CompanyName} requests ${req.RequestedNTE:F2} (was ${req.CurrentNTE ?? 0m:F2})";
var body = $@"
<h2>Vendor Uplift Request</h2>
<p><strong>Dispatch:</strong> {dispatchNum}</p>
<p><strong>Vendor:</strong> {vendorName}</p>
<table style='border-collapse:collapse;font-family:Arial,sans-serif;'>
<tr><td style='padding:4px 10px;'><strong>Current NTE</strong></td><td style='padding:4px 10px;'>${req.CurrentNTE ?? 0m:F2}</td></tr>
<tr><td style='padding:4px 10px;'><strong>Requested NTE</strong></td><td style='padding:4px 10px;'>${req.RequestedNTE:F2}</td></tr>
<tr><td style='padding:4px 10px;'><strong>Delta</strong></td><td style='padding:4px 10px;'>${(req.RequestedNTE - (req.CurrentNTE ?? 0m)):F2}</td></tr>
<tr><td style='padding:4px 10px;'><strong>Required Approval</strong></td><td style='padding:4px 10px;'>{tierText}</td></tr>
</table>
<h3>Vendor Reason</h3>
<p>{reason}</p>
<div style='margin:20px 0;'>
<a href='{workOrderLink}' style='display:inline-block;padding:10px 22px;background:#2563eb;color:white;text-decoration:none;border-radius:6px;font-weight:bold;'>Review in SHOC</a>
</div>";
await _sendMessage.SendEMail(dispatcher.Email, subject, body);
}
catch
{
}
}
private async Task<Vendor?> ResolveVendorAsync()
{
if (!Request.Headers.TryGetValue(TokenHeader, out var values)) return null;
var token = values.ToString();
return await _tokens.ResolveVendorAsync(token);
}
private Task<Dispatch?> LoadVendorDispatchAsync(int id, int vendorId)
{
return _db.Dispatches
.Include(d => d.WorkOrder).ThenInclude(w => w!.Locations)
.FirstOrDefaultAsync(d => d.Id == id
&& d.VendorId == vendorId
&& (d.IsDeleted == null || d.IsDeleted == false));
}
private static bool IsAllowedVendorTransition(string? from, string? to)
{
if (from == "Acknowledged" && to == "In Progress") return true;
if (from == "In Progress" && to == "Completed") return true;
return false;
}
public class ChangeStatusRequest
{
public string? Status { get; set; }
}
public class RequestCancelRequest
{
public string? Reason { get; set; }
}
public class ChecklistUpdateRequest
{
public bool IsCompleted { get; set; }
}
public class SignoffRequest
{
public string? Name { get; set; }
public string? Signature { get; set; }
public string? SignatureMethod { get; set; }
public string? SignoffType { get; set; }
}
public class CommentRequest
{
public string? CommentText { get; set; }
}
public class UpliftRequestBody
{
public decimal RequestedNTE { get; set; }
public string? Reason { get; set; }
}
}
}

View file

@ -1,23 +0,0 @@
using Microsoft.AspNetCore.Mvc;
namespace Api.SeaHavenIndustries.Controllers
{
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -2,10 +2,11 @@
{
public class Comments_DTO
{
public string Text { get; set; }
public string? Text { get; set; }
public IFormFile? Document { get; set; }
public int WorkorderId { get; set; }
public DateTime CreatedDate { get; set; }
public string UserId { get; set; }
public DateTime? CreatedDate { get; set; }
public string? UserId { get; set; }
public string? CommentType { get; set; }
}
}

View file

@ -1,4 +1,7 @@
namespace Api.SeaHavenIndustries.DTOs
using Data.SeaHavenIndustries;
using SeaHaven.Services.DTOs;
namespace Api.SeaHavenIndustries.DTOs
{
public class Contact_DTO
{
@ -20,5 +23,100 @@
public string? TwitterUrl { get; set; }
public int? LocationId { get; set; }
public int? AccountId { get; set; }
// ✅ Map API DTO to Service DTO
public CreateContactDTO ToServiceCreateDTO()
{
return new CreateContactDTO
{
FirstName = this.FirstName,
LastName = this.LastName,
Email = this.Email ?? "",
Phone = this.PhoneNumber,
Title = this.Title,
Type = this.ContactType,
AccountId = this.AccountId
};
}
// Map from Entity to DTO (Model)
public static Contact_DTO MapToModel(Contacts entity)
{
return new Contact_DTO
{
Title = entity.Title,
Owner = entity.Owner,
FirstName = entity.FirstName,
MiddleName = entity.MiddleName,
LastName = entity.LastName,
ContactType = entity.ContactType,
PhoneNumber = entity.PhoneNumber,
Email = entity.Email,
Address1 = entity.Address1,
Address2 = entity.Address2,
City = entity.City,
State = entity.State,
Zip = entity.Zip,
FacebookUrl = entity.FacebookUrl,
LinkedInUrl = entity.LinkedInUrl,
TwitterUrl = entity.TwitterUrl,
LocationId = entity.LocationId,
AccountId = entity.AccountId
};
}
// Map from DTO (Model) to Entity
public static Contacts MapToEntity(Contact_DTO dto, Contacts? existingEntity = null)
{
var entity = existingEntity ?? new Contacts();
entity.Title = dto.Title;
entity.Owner = dto.Owner;
entity.FirstName = dto.FirstName;
entity.MiddleName = dto.MiddleName;
entity.LastName = dto.LastName;
entity.ContactType = dto.ContactType;
entity.PhoneNumber = dto.PhoneNumber;
entity.Email = dto.Email;
entity.Address1 = dto.Address1;
entity.Address2 = dto.Address2;
entity.City = dto.City;
entity.State = dto.State;
entity.Zip = dto.Zip;
entity.FacebookUrl = dto.FacebookUrl;
entity.LinkedInUrl = dto.LinkedInUrl;
entity.TwitterUrl = dto.TwitterUrl;
entity.LocationId = dto.LocationId;
entity.AccountId = dto.AccountId;
return entity;
}
}
// DTO for contact list view
public class ContactListItem_DTO
{
public int Id { get; set; }
public string? Name { get; set; }
public string? Email { get; set; }
public string? Phone { get; set; }
// ✅ Map from Service DTO
public static ContactListItem_DTO FromServiceDTO(ContactDTO serviceDto)
{
return new ContactListItem_DTO
{
Id = serviceDto.Id,
Name = (serviceDto.FirstName + " " + serviceDto.LastName).Trim(),
Email = serviceDto.Email,
Phone = serviceDto.Phone
};
}
// ✅ Map collection from Service DTOs
public static IEnumerable<ContactListItem_DTO> FromServiceDTOs(IEnumerable<ContactDTO> serviceDtos)
{
return serviceDtos.Select(FromServiceDTO);
}
}
}

View file

@ -0,0 +1,11 @@
namespace Api.SeaHavenIndustries.DTOs
{
public class DispatchSignoff_DTO
{
public int DispatchId { get; set; }
public string? SignoffType { get; set; }
public string? Name { get; set; }
public string? Signature { get; set; }
public string? SignatureMethod { get; set; }
}
}

View file

@ -0,0 +1,27 @@
namespace Api.SeaHavenIndustries.DTOs
{
public class Dispatch_DTO
{
public List<int> WorkOrderIds { get; set; } = new();
public List<int> VendorIds { get; set; } = new();
public decimal? NTEAmount { get; set; }
public DateTime? ScheduledDate { get; set; }
public string? Description { get; set; }
public int? TaskListTemplateId { get; set; }
public List<string>? CustomChecklistItems { get; set; }
}
public class ChecklistItemUpdate_DTO
{
public int Id { get; set; }
public bool IsCompleted { get; set; }
public string? CompletedBy { get; set; }
}
public class AddChecklistItem_DTO
{
public int DispatchId { get; set; }
public int? WorkOrderId { get; set; }
public string? ItemText { get; set; }
}
}

View file

@ -1,4 +1,7 @@
namespace Api.SeaHavenIndustries.DTOs
using Data.SeaHavenIndustries;
using SeaHaven.Services.DTOs;
namespace Api.SeaHavenIndustries.DTOs
{
public class EditContact_DTO
{
@ -21,5 +24,72 @@
public string? TwitterUrl { get; set; }
public int? LocationId { get; set; }
public int? AccountId { get; set; }
// ✅ Map API DTO to Service DTO
public UpdateContactDTO ToServiceUpdateDTO()
{
return new UpdateContactDTO
{
FirstName = this.FirstName,
LastName = this.LastName,
Email = this.Email,
Phone = this.PhoneNumber,
Title = this.Title,
Type = this.ContactType,
AccountId = this.AccountId
};
}
// Map from Entity to DTO (Model)
public static EditContact_DTO MapToModel(Contacts entity)
{
return new EditContact_DTO
{
Id = entity.Id,
Title = entity.Title,
Owner = entity.Owner,
FirstName = entity.FirstName,
MiddleName = entity.MiddleName,
LastName = entity.LastName,
ContactType = entity.ContactType,
PhoneNumber = entity.PhoneNumber,
Email = entity.Email,
Address1 = entity.Address1,
Address2 = entity.Address2,
City = entity.City,
State = entity.State,
Zip = entity.Zip,
FacebookUrl = entity.FacebookUrl,
LinkedInUrl = entity.LinkedInUrl,
TwitterUrl = entity.TwitterUrl,
LocationId = entity.LocationId,
AccountId = entity.AccountId
};
}
// Map from DTO (Model) to Entity - updates existing entity
public static Contacts MapToEntity(EditContact_DTO dto, Contacts existingEntity)
{
existingEntity.Title = dto.Title;
existingEntity.Owner = dto.Owner;
existingEntity.FirstName = dto.FirstName;
existingEntity.MiddleName = dto.MiddleName;
existingEntity.LastName = dto.LastName;
existingEntity.ContactType = dto.ContactType;
existingEntity.PhoneNumber = dto.PhoneNumber;
existingEntity.Email = dto.Email;
existingEntity.Address1 = dto.Address1;
existingEntity.Address2 = dto.Address2;
existingEntity.City = dto.City;
existingEntity.State = dto.State;
existingEntity.Zip = dto.Zip;
existingEntity.FacebookUrl = dto.FacebookUrl;
existingEntity.LinkedInUrl = dto.LinkedInUrl;
existingEntity.TwitterUrl = dto.TwitterUrl;
existingEntity.LocationId = dto.LocationId;
existingEntity.AccountId = dto.AccountId;
return existingEntity;
}
}
}

View file

@ -1,9 +1,37 @@
namespace Api.SeaHavenIndustries.DTOs
using SeaHaven.Services.DTOs;
namespace Api.SeaHavenIndustries.DTOs
{
public class EditLocation_DTO
{
public int Id { get; set; }
public string? Title { get; set; }
public string? Name { get; set; }
public string? Address { get; set; }
public string? City { get; set; }
public string? State { get; set; }
public string? ZipCode { get; set; }
public string? Phone { get; set; }
public string? Contact { get; set; }
public string? ContactEmail { get; set; }
public string? Status { get; set; }
public string? AccountId { get; set; }
public int? GetAccountId() =>
int.TryParse(AccountId, out var id) ? id : null;
// ✅ Map API DTO to Service DTO
public UpdateLocationDTO ToServiceUpdateDTO()
{
return new UpdateLocationDTO
{
LocationName = this.Name,
Address = this.Address,
City = this.City,
State = this.State,
Zipcode = this.ZipCode,
AccountId = this.GetAccountId()
};
}
}
}

View file

@ -1,8 +1,135 @@
namespace Api.SeaHavenIndustries.DTOs
using Data.SeaHavenIndustries;
using SeaHaven.Services.DTOs;
namespace Api.SeaHavenIndustries.DTOs
{
public class Location_DTO
{
public string? Title { get; set; }
public string? Name { get; set; }
public string? Address { get; set; }
public string? City { get; set; }
public string? State { get; set; }
public string? ZipCode { get; set; }
public string? Phone { get; set; }
public string? Contact { get; set; }
public string? ContactEmail { get; set; }
public string? Status { get; set; }
public string? AccountId { get; set; }
public int? GetAccountId() =>
int.TryParse(AccountId, out var id) ? id : null;
// ✅ Map API DTO to Service DTO
public CreateLocationDTO ToServiceCreateDTO()
{
return new CreateLocationDTO
{
LocationName = this.Name ?? "",
Address = this.Address,
City = this.City,
State = this.State,
Zipcode = this.ZipCode,
AccountId = this.GetAccountId()
};
}
// Map from Entity to DTO
public static Location_DTO MapToModel(Locations entity)
{
return new Location_DTO
{
Title = entity.Title,
Name = entity.Name,
Address = entity.Address1,
City = entity.City,
State = entity.State,
ZipCode = entity.Zip,
Phone = entity.PhoneNumber,
Contact = null, // Not in database schema
ContactEmail = entity.Email,
Status = entity.Status,
AccountId = null // Not in database schema
};
}
// Map from DTO to Entity
public static Locations MapToEntity(Location_DTO dto, Locations? existingEntity = null)
{
var entity = existingEntity ?? new Locations();
entity.Title = dto.Title;
entity.Name = dto.Name;
entity.Address1 = dto.Address;
entity.City = dto.City;
entity.State = dto.State;
entity.Zip = dto.ZipCode;
entity.PhoneNumber = dto.Phone;
// Contact field doesn't exist in database schema - ignored
entity.Email = dto.ContactEmail;
entity.Status = dto.Status;
// AccountId field doesn't exist in database schema - ignored
return entity;
}
}
// DTO for list views
public class LocationListItem_DTO
{
public int Id { get; set; }
public string? Name { get; set; }
public string? Title { get; set; }
// Map from Entity
public static LocationListItem_DTO MapToModel(Locations entity)
{
return new LocationListItem_DTO
{
Id = entity.Id,
Name = entity.Name,
Title = entity.Title
};
}
// ✅ Map from Service DTO (no logic in controller)
public static LocationListItem_DTO FromServiceDTO(LocationDTO serviceDto)
{
return new LocationListItem_DTO
{
Id = serviceDto.Id,
Name = serviceDto.Name ?? serviceDto.Title,
Title = serviceDto.Title ?? serviceDto.Name
};
}
// ✅ Map collection from Service DTOs
public static IEnumerable<LocationListItem_DTO> FromServiceDTOs(IEnumerable<LocationDTO> serviceDtos)
{
return serviceDtos.Select(FromServiceDTO);
}
}
// DTO for location dropdown/simple view
public class LocationViewModel_DTO
{
public int? Id { get; set; }
public string? Name { get; set; }
// ✅ Map from Service DTO
public static LocationViewModel_DTO FromServiceDTO(LocationDTO serviceDto)
{
return new LocationViewModel_DTO
{
Id = serviceDto.Id,
Name = serviceDto.Name ?? serviceDto.Title
};
}
// ✅ Map collection from Service DTOs
public static IEnumerable<LocationViewModel_DTO> FromServiceDTOs(IEnumerable<LocationDTO> serviceDtos)
{
return serviceDtos.Select(FromServiceDTO);
}
}
}

View file

@ -0,0 +1,16 @@
namespace Api.SeaHavenIndustries.DTOs
{
public class TaskListTemplate_DTO
{
public string? Name { get; set; }
public string? Description { get; set; }
public bool IsActive { get; set; } = true;
public List<TaskListTemplateItem_DTO> Items { get; set; } = new();
}
public class TaskListTemplateItem_DTO
{
public string? ItemText { get; set; }
public int SortOrder { get; set; }
}
}

View file

@ -0,0 +1,19 @@
namespace Api.SeaHavenIndustries.DTOs
{
public class UpdateDispatch_DTO
{
public int Id { get; set; }
public string? Status { get; set; }
public decimal? NTEAmount { get; set; }
public DateTime? ScheduledDate { get; set; }
public DateTime? CompletedDate { get; set; }
public string? Description { get; set; }
}
public class DispatchComment_DTO
{
public int DispatchId { get; set; }
public string? Text { get; set; }
public bool SendEmail { get; set; } = true;
}
}

View file

@ -0,0 +1,21 @@
namespace Api.SeaHavenIndustries.DTOs
{
public class Vendor_DTO
{
public string? CompanyName { get; set; }
public string? ContactName { get; set; }
public string? Email { get; set; }
public string? Phone { get; set; }
public string? Address { get; set; }
public string? City { get; set; }
public string? State { get; set; }
public string? Zip { get; set; }
public string? TradeSpecialties { get; set; }
public bool IsActive { get; set; } = true;
}
public class EditVendor_DTO : Vendor_DTO
{
public int Id { get; set; }
}
}

View file

@ -44,12 +44,18 @@ namespace Api.SeaHavenIndustries.DTOs
public string? Description { get; set; }
public string? PO { get; set; }
public string? TT { get; set; }
//public int? ContactId { get; set; }
public string? AssignTo { get; set; }
public DateTime? DueDate { get; set; }
public string? Priority { get; set; }
public string? Status { get; set; }
public int? LocationId { get; set; }
//public int? CategoryId { get; set; }
public string? Problem { get; set; }
public string? Trade { get; set; }
public string? SubTrade { get; set; }
public decimal? VendorNTE { get; set; }
public DateTime? ScheduledDate { get; set; }
public DateTime? CompletedDate { get; set; }
public string? Source { get; set; }
public List<IFormFile>? Attachments { get; set; }
public string? BeforPhotoIssue { get; set; }

View file

@ -98,8 +98,27 @@ namespace Api.SeaHavenIndustries.Helper
return false;
}
}
public async Task<bool> SendDispatchEmail(string emailTo, string subject, string htmlBody, string? replyTo)
{
try
{
var apiKey = _configuration.GetValue<string>("SendGrid:ApiKey");
var client = new SendGridClient(apiKey);
var from = new EmailAddress("tech@seahavenind.com", "Sea Haven Industries");
var to = new EmailAddress(emailTo, "");
var msg = MailHelper.CreateSingleEmail(from, to, subject, "", htmlBody);
if (!string.IsNullOrWhiteSpace(replyTo))
msg.SetReplyTo(new EmailAddress(replyTo));
var response = await client.SendEmailAsync(msg);
return true;
}
catch (Exception ex)
{
return false;
}
}
}
}

View file

@ -0,0 +1,92 @@
using System.Security.Cryptography;
using Data.SeaHavenIndustries;
using Microsoft.EntityFrameworkCore;
namespace Api.SeaHavenIndustries.Helper
{
public class VendorPortalTokenService
{
private readonly ApplicationDbContext _db;
private readonly int _lifetimeDays;
public VendorPortalTokenService(ApplicationDbContext db, IConfiguration config)
{
_db = db;
_lifetimeDays = config.GetValue<int?>("VendorPortal:TokenLifetimeDays") ?? 365;
}
public async Task<VendorAccessToken> GetOrCreateActiveTokenAsync(int vendorId)
{
var now = DateTime.UtcNow;
var existing = await _db.VendorAccessTokens
.Where(t => t.VendorId == vendorId
&& t.RevokedAt == null
&& t.ExpiresAt > now
&& (t.IsDeleted == null || t.IsDeleted == false))
.OrderByDescending(t => t.IssuedAt)
.FirstOrDefaultAsync();
if (existing != null) return existing;
var token = new VendorAccessToken
{
VendorId = vendorId,
Token = GenerateToken(),
IssuedAt = now,
ExpiresAt = now.AddDays(_lifetimeDays),
CreatedDate = now
};
_db.VendorAccessTokens.Add(token);
await _db.SaveChangesAsync();
return token;
}
public async Task<Vendor?> ResolveVendorAsync(string? token)
{
if (string.IsNullOrWhiteSpace(token)) return null;
var now = DateTime.UtcNow;
var record = await _db.VendorAccessTokens
.Include(t => t.Vendor)
.FirstOrDefaultAsync(t => t.Token == token
&& t.RevokedAt == null
&& t.ExpiresAt > now
&& (t.IsDeleted == null || t.IsDeleted == false));
if (record?.Vendor == null) return null;
record.LastUsedAt = now;
await _db.SaveChangesAsync();
return record.Vendor;
}
public async Task<VendorAccessToken> RotateAsync(int vendorId)
{
var now = DateTime.UtcNow;
var active = await _db.VendorAccessTokens
.Where(t => t.VendorId == vendorId && t.RevokedAt == null)
.ToListAsync();
foreach (var t in active) t.RevokedAt = now;
await _db.SaveChangesAsync();
return await GetOrCreateActiveTokenAsync(vendorId);
}
public async Task RevokeAllAsync(int vendorId)
{
var now = DateTime.UtcNow;
var active = await _db.VendorAccessTokens
.Where(t => t.VendorId == vendorId && t.RevokedAt == null)
.ToListAsync();
foreach (var t in active) t.RevokedAt = now;
await _db.SaveChangesAsync();
}
private static string GenerateToken()
{
var bytes = RandomNumberGenerator.GetBytes(32);
return Convert.ToBase64String(bytes)
.Replace("+", "-")
.Replace("/", "_")
.TrimEnd('=');
}
}
}

View file

@ -0,0 +1,48 @@
namespace Api.SeaHavenIndustries.Helper
{
public class ZipCodeDistance
{
private readonly Dictionary<string, (double lat, double lng)> _coords = new();
private bool _loaded;
public void Load(string csvPath)
{
if (_loaded) return;
foreach (var line in File.ReadLines(csvPath).Skip(1))
{
var parts = line.Split(',');
if (parts.Length >= 3
&& double.TryParse(parts[1].Trim(), out var lat)
&& double.TryParse(parts[2].Trim(), out var lng))
{
_coords[parts[0].Trim().PadLeft(5, '0')] = (lat, lng);
}
}
_loaded = true;
}
public double? GetDistanceMiles(string? zip1, string? zip2)
{
if (string.IsNullOrWhiteSpace(zip1) || string.IsNullOrWhiteSpace(zip2)) return null;
var z1 = zip1.Trim().PadLeft(5, '0');
var z2 = zip2.Trim().PadLeft(5, '0');
if (z1.Length > 5) z1 = z1.Substring(0, 5);
if (z2.Length > 5) z2 = z2.Substring(0, 5);
if (!_coords.TryGetValue(z1, out var c1) || !_coords.TryGetValue(z2, out var c2)) return null;
return Haversine(c1.lat, c1.lng, c2.lat, c2.lng);
}
private static double Haversine(double lat1, double lon1, double lat2, double lon2)
{
const double R = 3958.8;
var dLat = ToRad(lat2 - lat1);
var dLon = ToRad(lon2 - lon1);
var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) +
Math.Cos(ToRad(lat1)) * Math.Cos(ToRad(lat2)) *
Math.Sin(dLon / 2) * Math.Sin(dLon / 2);
return R * 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
}
private static double ToRad(double deg) => deg * Math.PI / 180.0;
}
}

View file

@ -31,7 +31,49 @@ builder.Services.AddResponseCompression(opts =>
});
builder.Services.AddCors(option =>
option.AddDefaultPolicy(builder => builder.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod()));
// Helper services
builder.Services.AddScoped<SendMessage>();
builder.Services.AddScoped<Api.SeaHavenIndustries.Helper.VendorPortalTokenService>();
builder.Services.AddSingleton<Api.SeaHavenIndustries.Helper.ZipCodeDistance>(sp =>
{
var zipService = new Api.SeaHavenIndustries.Helper.ZipCodeDistance();
var env = sp.GetRequiredService<IWebHostEnvironment>();
var csvPath = Path.Combine(env.WebRootPath, "zipcodes.csv");
if (File.Exists(csvPath)) zipService.Load(csvPath);
return zipService;
});
// ========================================================================
// MULTI-TIER ARCHITECTURE - Dependency Injection Registration
// ========================================================================
// Data Services (Data Access Layer)
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.IContactDataService, SeaHaven.DataServices.Implementation.ContactDataService>();
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.IWorkOrderDataService, SeaHaven.DataServices.Implementation.WorkOrderDataService>();
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.ILocationDataService, SeaHaven.DataServices.Implementation.LocationDataService>();
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.IEmployeeDataService, SeaHaven.DataServices.Implementation.EmployeeDataService>();
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.IAccountDataService, SeaHaven.DataServices.Implementation.AccountDataService>();
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.IVendorDataService, SeaHaven.DataServices.Implementation.VendorDataService>();
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.IDispatchDataService, SeaHaven.DataServices.Implementation.DispatchDataService>();
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.IFollowUpDataService, SeaHaven.DataServices.Implementation.FollowUpDataService>();
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.IPMScheduleDataService, SeaHaven.DataServices.Implementation.PMScheduleDataService>();
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.IAssetDataService, SeaHaven.DataServices.Implementation.AssetDataService>();
builder.Services.AddScoped<SeaHaven.DataServices.Interfaces.ICategoryDataService, SeaHaven.DataServices.Implementation.CategoryDataService>();
// Business Services (Business Logic Layer)
builder.Services.AddScoped<SeaHaven.Services.Interfaces.IContactService, SeaHaven.Services.Implementation.ContactService>();
builder.Services.AddScoped<SeaHaven.Services.Interfaces.IWorkOrderService, SeaHaven.Services.Implementation.WorkOrderService>();
builder.Services.AddScoped<SeaHaven.Services.Interfaces.ILocationService, SeaHaven.Services.Implementation.LocationService>();
builder.Services.AddScoped<SeaHaven.Services.Interfaces.IEmployeeService, SeaHaven.Services.Implementation.EmployeeService>();
builder.Services.AddScoped<SeaHaven.Services.Interfaces.IAccountService, SeaHaven.Services.Implementation.AccountService>();
builder.Services.AddScoped<SeaHaven.Services.Interfaces.IVendorService, SeaHaven.Services.Implementation.VendorService>();
builder.Services.AddScoped<SeaHaven.Services.Interfaces.IDispatchService, SeaHaven.Services.Implementation.DispatchService>();
builder.Services.AddScoped<SeaHaven.Services.Interfaces.IFollowUpService, SeaHaven.Services.Implementation.FollowUpService>();
builder.Services.AddScoped<SeaHaven.Services.Interfaces.IPMScheduleService, SeaHaven.Services.Implementation.PMScheduleService>();
builder.Services.AddScoped<SeaHaven.Services.Interfaces.IAssetService, SeaHaven.Services.Implementation.AssetService>();
// ========================================================================
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
@ -93,7 +135,8 @@ if (app.Environment.IsDevelopment() || app.Environment.IsProduction())
});
}
app.UseHttpsRedirection();
if (!app.Environment.IsDevelopment())
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCors();
app.UseAuthentication();
@ -101,4 +144,60 @@ app.UseAuthorization();
app.MapControllers();
//if (app.Environment.IsDevelopment())
//{
// using var scope = app.Services.CreateScope();
// var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
// var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();
// if (!await roleManager.RoleExistsAsync("Admin"))
// await roleManager.CreateAsync(new IdentityRole("Admin"));
// if (!await roleManager.RoleExistsAsync("Dispatcher"))
// await roleManager.CreateAsync(new IdentityRole("Dispatcher"));
// if (!await roleManager.RoleExistsAsync("Manager"))
// await roleManager.CreateAsync(new IdentityRole("Manager"));
// var adminEmail = "admin@seahavenind.com";
// if (await userManager.FindByNameAsync(adminEmail) == null)
// {
// var admin = new ApplicationUser
// {
// UserName = adminEmail,
// Email = adminEmail,
// FirstName = "Admin",
// LastName = "User",
// EmailConfirmed = true,
// UniqueName = "Active",
// CreatedDate = DateTime.Now
// };
// var result = await userManager.CreateAsync(admin, "Admin@123");
// if (result.Succeeded)
// await userManager.AddToRoleAsync(admin, "Admin");
// }
// var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// if (!db.DropdownOptions.Any())
// {
// var trades = new[] { "Plumbing", "Electrical", "HVAC", "Fire/Life Safety", "General", "Roofing", "Janitorial", "Painting", "Locksmith", "Carpentry" };
// for (int i = 0; i < trades.Length; i++)
// db.DropdownOptions.Add(new DropdownOption { Category = "Trade", Value = trades[i], SortOrder = i });
// var subTrades = new (string trade, string sub)[] {
// ("Plumbing", "Drain Cleaning"), ("Plumbing", "Pipe Repair"), ("Plumbing", "Fixture Install"), ("Plumbing", "Backflow Testing"),
// ("Electrical", "Lighting"), ("Electrical", "Panel/Breaker"), ("Electrical", "Wiring"), ("Electrical", "Generator"),
// ("HVAC", "AC Repair"), ("HVAC", "Heating"), ("HVAC", "Ductwork"), ("HVAC", "Controls/BMS"),
// ("Fire/Life Safety", "Sprinkler"), ("Fire/Life Safety", "Fire Alarm"), ("Fire/Life Safety", "Extinguisher"), ("Fire/Life Safety", "Fire Pump"),
// ("General", "Doors"), ("General", "Flooring"), ("General", "Drywall"), ("General", "Concrete"),
// };
// for (int i = 0; i < subTrades.Length; i++)
// db.DropdownOptions.Add(new DropdownOption { Category = "SubTrade", Value = subTrades[i].sub, ParentValue = subTrades[i].trade, SortOrder = i });
// var problems = new[] { "Leak", "No Power", "Not Cooling", "Not Heating", "Clogged", "Broken/Damaged", "Alarm Fault", "Preventive Maintenance", "Inspection", "Installation", "Other" };
// for (int i = 0; i < problems.Length; i++)
// db.DropdownOptions.Add(new DropdownOption { Category = "Problem", Value = problems[i], SortOrder = i });
// await db.SaveChangesAsync();
// }
//}
app.Run();

View file

@ -1,8 +1,26 @@
{
"ConnectionStrings": {
//"DefaultConnection": "Server=DESKTOP-64LC14O\SQLEXPRESS;Database=SeahavenIndustries;TrustServerCertificate=True;Trusted_Connection=True;MultipleActiveResultSets=true",
// Test database
//"DefaultConnection": "Server=SQL5112.site4now.net;Initial Catalog=db_a7141e_seahavenindustries;User Id=db_a7141e_seahavenindustries_admin;Password=Abc123!@#;MultipleActiveResultSets=true"
//Client Database
"DefaultConnection": "Server=SQL5112.site4now.net;Initial Catalog=db_a7141e_seahavenindustry;User Id=db_a7141e_seahavenindustry_admin;Password=Abc123!@#;MultipleActiveResultSets=true"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"SendGrid": {
"ApiKey": "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"
},
"AllowedHosts": "*",
"JWT": {
"ValidAudience": "http://localhost:4200",
"ValidIssuer": "http://localhost:7195",
"Secret": "JWTAuthenticationHIGHsecuredPasswordVVVp1OH7Xzyr"
}
}
}

View file

@ -0,0 +1,20 @@
{
"ConnectionStrings": {
"DefaultConnection": "Server=SQL5112.site4now.net;Initial Catalog=db_a7141e_seahavenindustry;User Id=db_a7141e_seahavenindustry_admin;Password=Abc123!@#;MultipleActiveResultSets=true"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"SendGrid": {
"ApiKey": "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"
},
"AllowedHosts": "*",
"JWT": {
"ValidAudience": "http://console.seahavenind.com",
"ValidIssuer": "http://console.seahavenind.com",
"Secret": "JWTAuthenticationHIGHsecuredPasswordVVVp1OH7Xzyr"
}
}

View file

@ -23,4 +23,4 @@
"ValidIssuer": "http://localhost:7195",
"Secret": "JWTAuthenticationHIGHsecuredPasswordVVVp1OH7Xzyr"
}
}
}

View file

@ -1,13 +1,12 @@
{
"region" : "us-east-1",
"configuration" : "Debug",
"framework" : "net8.0",
"self-contained" : true,
"application" : "SeaHavenIndustries",
"environment" : "SeaHavenIndustries-dev",
"enable-xray" : false,
"enhanced-health-type" : "basic",
"additional-options" : "",
"proxy-server" : "nginx"
"region": "us-east-1",
"configuration": "Debug",
"framework": "net8.0",
"self-contained": true,
"application": "SeaHavenIndustries",
"environment": "SeaHavenIndustries-dev",
"enable-xray": false,
"enhanced-health-type": "basic",
"additional-options": "",
"proxy-server": "nginx"
}

View file

@ -924,6 +924,206 @@ PRIMARY KEY CLUSTERED
GO
-- ==================================================================
-- DATABASE CHANGES APPLIED ON 2026-04-22
-- Migration: Calendar Module + Missing Column Fixes
-- Applied by: AI Agent
-- ==================================================================
-- ==================================================================
-- 1. EVENTS TABLE (Calendar Module)
-- Migration: 20260422021353_AddEventsTable
-- ==================================================================
USE [db_a7141e_seahavenindustry]
GO
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Events]') AND type in (N'U'))
BEGIN
CREATE TABLE [dbo].[Events](
[Id] [int] IDENTITY(1,1) NOT NULL,
-- Basic Event Information
[Title] [nvarchar](100) NOT NULL,
[Description] [nvarchar](max) NULL,
[Location] [nvarchar](150) NULL,
-- Related Record (Link to Accounts, Contacts, Work Orders, etc.)
[RecordID] [int] NULL,
[RecordName] [nvarchar](max) NULL,
[RecordTargetType] [nvarchar](max) NULL,
-- Reminder & Display Settings
[ReminderMinutes] [int] NOT NULL CONSTRAINT [DF_Events_ReminderMinutes] DEFAULT ((15)),
[EventColor] [nvarchar](20) NOT NULL CONSTRAINT [DF_Events_EventColor] DEFAULT (N'primary'),
-- Schedule Information
[StartDate] [datetime2](7) NOT NULL,
[StartTime] [nvarchar](10) NULL,
[EndDate] [datetime2](7) NOT NULL,
[EndTime] [nvarchar](10) NULL,
[AllDay] [bit] NOT NULL CONSTRAINT [DF_Events_AllDay] DEFAULT ((0)),
-- Audit Fields (from FullAuditEntity)
[IsDeleted] [bit] NOT NULL CONSTRAINT [DF_Events_IsDeleted] DEFAULT ((0)),
[DeletionTime] [datetime2](7) NULL,
[createdby] [nvarchar](max) NULL,
[CreatedDate] [datetime2](7) NULL,
[lastmodifiedby] [nvarchar](max) NULL,
[LastModificationTime] [datetime2](7) NULL,
CONSTRAINT [PK_Events] PRIMARY KEY CLUSTERED ([Id] ASC)
WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
END
GO
-- Create Index on StartDate for faster calendar queries
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Events_StartDate' AND object_id = OBJECT_ID('[dbo].[Events]'))
BEGIN
CREATE NONCLUSTERED INDEX [IX_Events_StartDate] ON [dbo].[Events]
(
[StartDate] ASC
)
WHERE ([IsDeleted] = 0)
END
GO
-- Create Index on IsDeleted for soft delete queries
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Events_IsDeleted' AND object_id = OBJECT_ID('[dbo].[Events]'))
BEGIN
CREATE NONCLUSTERED INDEX [IX_Events_IsDeleted] ON [dbo].[Events]
(
[IsDeleted] ASC
)
END
GO
-- ==================================================================
-- 2. WORKORDERS TABLE - MISSING COLUMNS
-- Migrations: 20260416212330, 20260416224015, 20260417142453
-- ==================================================================
-- Migration: 20260416224015_AddInternalWONumber
ALTER TABLE [dbo].[WorkOrders]
ADD [InternalWONumber] [nvarchar](max) NULL
GO
-- Migration: 20260416212330_AddExternalSyncFields
ALTER TABLE [dbo].[WorkOrders]
ADD [ExternalWorkOrderId] [nvarchar](max) NULL,
[Source] [nvarchar](max) NULL
GO
-- Migration: 20260417142453_AddDropdownOptionsAndWOFields
ALTER TABLE [dbo].[WorkOrders]
ADD [Problem] [nvarchar](max) NULL,
[Trade] [nvarchar](max) NULL,
[SubTrade] [nvarchar](max) NULL,
[VendorNTE] [decimal](18, 2) NULL,
[ScheduledDate] [datetime2](7) NULL,
[CompletedDate] [datetime2](7) NULL
GO
-- ==================================================================
-- 3. COMMENTS TABLE - MISSING COLUMNS
-- Migrations: 20260416220656, 20260417171152
-- ==================================================================
-- Migration: 20260416220656_AddCommentTypeAndAuditLog
ALTER TABLE [dbo].[Comments]
ADD [CommentType] [nvarchar](max) NULL,
[Commenter] [nvarchar](max) NULL,
[RecordType] [nvarchar](max) NULL
GO
-- Migration: 20260417171152_AddDispatchNumberAndCommentDispatchId
ALTER TABLE [dbo].[Comments]
ADD [DispatchId] [int] NULL
GO
-- ==================================================================
-- 4. LOCATIONS TABLE - MISSING COLUMNS
-- Migration: 20260416210833_AddLocationFields
-- ==================================================================
ALTER TABLE [dbo].[Locations]
ADD [Address1] [nvarchar](max) NULL,
[Address2] [nvarchar](max) NULL,
[City] [nvarchar](max) NULL,
[State] [nvarchar](max) NULL,
[Zip] [nvarchar](max) NULL,
[PhoneNumber] [nvarchar](max) NULL,
[Email] [nvarchar](max) NULL,
[Status] [nvarchar](max) NULL
GO
-- ==================================================================
-- 5. MIGRATION HISTORY RECORDS
-- Marked these migrations as applied to sync with code
-- ==================================================================
INSERT INTO [dbo].[__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES
(N'20260416204109_SyncContactsAndModels', N'8.0.8'),
(N'20260416210833_AddLocationFields', N'8.0.8'),
(N'20260416212330_AddExternalSyncFields', N'8.0.8'),
(N'20260416220656_AddCommentTypeAndAuditLog', N'8.0.8'),
(N'20260416224015_AddInternalWONumber', N'8.0.8'),
(N'20260417142453_AddDropdownOptionsAndWOFields', N'8.0.8'),
(N'20260417153405_AddVendorAndDispatch', N'8.0.8'),
(N'20260417154641_AddDispatchScheduledDate', N'8.0.8'),
(N'20260417171152_AddDispatchNumberAndCommentDispatchId', N'8.0.8'),
(N'20260417182910_AddTaskListTemplates', N'8.0.8'),
(N'20260417190743_MultiWODispatch', N'8.0.8'),
(N'20260417192012_AddDispatchChecklist', N'8.0.8'),
(N'20260417194438_AddDispatchSignoffs', N'8.0.8'),
(N'20260417195316_AddDispatchVerification', N'8.0.8'),
(N'20260420151445_AddVendorAccessToken', N'8.0.8'),
(N'20260420164708_AddDispatchUpliftRequest', N'8.0.8'),
(N'20260422021353_AddEventsTable', N'8.0.8')
GO
-- ==================================================================
-- SUMMARY OF CHANGES (2026-04-22)
-- ==================================================================
-- Tables Created (11):
-- 1. Events (Calendar/Events module)
-- 2. WorkOrderAuditLogs (Audit trail for work order changes)
-- 3. DropdownOptions (Dynamic dropdown values)
-- 4. Vendors (Vendor management)
-- 5. Dispatches (Vendor dispatch workflow)
-- 6. TaskListTemplates (Reusable task templates)
-- 7. TaskListTemplateItems (Template task items)
-- 8. DispatchWorkOrders (Many-to-many: Dispatch <-> WorkOrders)
-- 9. DispatchChecklistItems (Dispatch checklist)
-- 10. DispatchSignoffs (Customer & Vendor signatures)
-- 11. DispatchUpliftRequests (NTE increase requests)
--
-- Columns Added to WorkOrders (9):
-- - InternalWONumber, ExternalWorkOrderId, Source
-- - Problem, Trade, SubTrade
-- - VendorNTE, ScheduledDate, CompletedDate
--
-- Columns Added to Comments (4):
-- - CommentType, Commenter, RecordType, DispatchId
--
-- Columns Added to Locations (8):
-- - Address1, Address2, City, State, Zip
-- - PhoneNumber, Email, Status
--
-- Columns Added to Dispatches (7):
-- - ScheduledDate, DispatchNumber, CompletedDate
-- - VerifiedBy, VerifiedAt, AcceptToken, AcknowledgedAt
--
-- Columns Added to Vendors (1):
-- - AccessToken
--
-- Migrations Applied: 17
-- From: 20260416204109_SyncContactsAndModels
-- To: 20260422021353_AddEventsTable
--
-- Database Status: UP TO DATE ✓
-- ==================================================================

File diff suppressed because it is too large Load diff

View file

@ -24,6 +24,15 @@ namespace Data.SeaHavenIndustries
{
foreignKey.DeleteBehavior = DeleteBehavior.Restrict;
}
builder.Entity<VendorAccessToken>()
.HasIndex(t => t.Token)
.IsUnique();
builder.Entity<DispatchUpliftRequest>()
.HasIndex(u => new { u.DispatchId, u.Status })
.IsUnique()
.HasFilter("[Status] = 'Pending'");
}
public DbSet<Category> Categories { get; set; }
public DbSet<Locations> Locations { get; set; }
@ -37,6 +46,17 @@ namespace Data.SeaHavenIndustries
public DbSet<WorkOrderContacts> WorkOrderContacts { get; set; }
public DbSet<WorkOrderCategories> workOrderCategories { get; set; }
public DbSet<WorkOrderAttachments> workOrderAttachments { get; set; }
public DbSet<WorkOrderAuditLog> WorkOrderAuditLogs { get; set; }
public DbSet<DropdownOption> DropdownOptions { get; set; }
public DbSet<Vendor> Vendors { get; set; }
public DbSet<VendorAccessToken> VendorAccessTokens { get; set; }
public DbSet<Dispatch> Dispatches { get; set; }
public DbSet<DispatchWorkOrder> DispatchWorkOrders { get; set; }
public DbSet<DispatchChecklistItem> DispatchChecklistItems { get; set; }
public DbSet<DispatchSignoff> DispatchSignoffs { get; set; }
public DbSet<DispatchUpliftRequest> DispatchUpliftRequests { get; set; }
public DbSet<TaskListTemplate> TaskListTemplates { get; set; }
public DbSet<TaskListTemplateItem> TaskListTemplateItems { get; set; }
// New DbSets for added entities
public DbSet<Accounts> Accounts { get; set; }

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,852 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.SeaHavenIndustries.Migrations
{
/// <inheritdoc />
public partial class SyncContactsAndModels : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "Name",
table: "Contacts",
newName: "Zip");
migrationBuilder.AddColumn<int>(
name: "AccountId",
table: "Contacts",
type: "int",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Address1",
table: "Contacts",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Address2",
table: "Contacts",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "City",
table: "Contacts",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "ContactType",
table: "Contacts",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "FacebookUrl",
table: "Contacts",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "FirstName",
table: "Contacts",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "LastName",
table: "Contacts",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "LinkedInUrl",
table: "Contacts",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "LocationId",
table: "Contacts",
type: "int",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "MiddleName",
table: "Contacts",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Owner",
table: "Contacts",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "State",
table: "Contacts",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Title",
table: "Contacts",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "TwitterUrl",
table: "Contacts",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "AspNetUserTokens",
type: "nvarchar(450)",
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(128)",
oldMaxLength: 128);
migrationBuilder.AlterColumn<string>(
name: "LoginProvider",
table: "AspNetUserTokens",
type: "nvarchar(450)",
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(128)",
oldMaxLength: 128);
migrationBuilder.AlterColumn<string>(
name: "ProviderKey",
table: "AspNetUserLogins",
type: "nvarchar(450)",
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(128)",
oldMaxLength: 128);
migrationBuilder.AlterColumn<string>(
name: "LoginProvider",
table: "AspNetUserLogins",
type: "nvarchar(450)",
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(128)",
oldMaxLength: 128);
migrationBuilder.CreateTable(
name: "Accounts",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Name = table.Column<string>(type: "nvarchar(max)", nullable: true),
Type = table.Column<string>(type: "nvarchar(max)", nullable: true),
Importance = table.Column<int>(type: "int", nullable: true),
TerritoryId = table.Column<int>(type: "int", nullable: true),
OwnerEmployeeId = table.Column<int>(type: "int", nullable: true),
PrivacySetting = table.Column<string>(type: "nvarchar(max)", nullable: true),
About = table.Column<string>(type: "nvarchar(max)", nullable: true),
Tags = table.Column<string>(type: "nvarchar(max)", nullable: true),
Phone = table.Column<string>(type: "nvarchar(max)", nullable: true),
Email = table.Column<string>(type: "nvarchar(max)", nullable: true),
IndustryId = table.Column<int>(type: "int", nullable: true),
TimeZone = table.Column<string>(type: "nvarchar(max)", nullable: true),
Website = table.Column<string>(type: "nvarchar(max)", nullable: true),
CurrencyCode = table.Column<string>(type: "nvarchar(max)", nullable: true),
ServiceInstructions = table.Column<string>(type: "nvarchar(max)", nullable: true),
FacebookUrl = table.Column<string>(type: "nvarchar(max)", nullable: true),
LinkedInUrl = table.Column<string>(type: "nvarchar(max)", nullable: true),
TwitterUrl = table.Column<string>(type: "nvarchar(max)", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: true),
createdby = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeleterUserId = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierUserId = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Accounts", x => x.Id);
});
migrationBuilder.CreateTable(
name: "ContactDetails",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
ContactId = table.Column<int>(type: "int", nullable: false),
Gender = table.Column<string>(type: "nvarchar(max)", nullable: true),
DateOfBirth = table.Column<DateTime>(type: "datetime2", nullable: true),
Territory = table.Column<string>(type: "nvarchar(max)", nullable: true),
PrivacySetting = table.Column<string>(type: "nvarchar(max)", nullable: true),
About = table.Column<string>(type: "nvarchar(max)", nullable: true),
Tags = table.Column<string>(type: "nvarchar(max)", nullable: true),
FacebookUrl = table.Column<string>(type: "nvarchar(max)", nullable: true),
LinkedInUrl = table.Column<string>(type: "nvarchar(max)", nullable: true),
TwitterUrl = table.Column<string>(type: "nvarchar(max)", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: true),
createdby = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeleterUserId = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierUserId = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ContactDetails", x => x.Id);
table.ForeignKey(
name: "FK_ContactDetails_Contacts_ContactId",
column: x => x.ContactId,
principalTable: "Contacts",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "Departments",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
IsDeleted = table.Column<bool>(type: "bit", nullable: true),
createdby = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeleterUserId = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierUserId = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Departments", x => x.Id);
});
migrationBuilder.CreateTable(
name: "JobTitles",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
IsDeleted = table.Column<bool>(type: "bit", nullable: true),
createdby = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeleterUserId = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierUserId = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_JobTitles", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Regions",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
IsDeleted = table.Column<bool>(type: "bit", nullable: true),
createdby = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeleterUserId = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierUserId = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Regions", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Addresses",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
AddressType = table.Column<string>(type: "nvarchar(max)", nullable: true),
Address1 = table.Column<string>(type: "nvarchar(max)", nullable: true),
Address2 = table.Column<string>(type: "nvarchar(max)", nullable: true),
City = table.Column<string>(type: "nvarchar(max)", nullable: true),
State = table.Column<string>(type: "nvarchar(max)", nullable: true),
Zip = table.Column<string>(type: "nvarchar(max)", nullable: true),
Country = table.Column<string>(type: "nvarchar(max)", nullable: true),
County = table.Column<string>(type: "nvarchar(max)", nullable: true),
AccountId = table.Column<int>(type: "int", nullable: true),
ContactId = table.Column<int>(type: "int", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: true),
createdby = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeleterUserId = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierUserId = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Addresses", x => x.Id);
table.ForeignKey(
name: "FK_Addresses_Accounts_AccountId",
column: x => x.AccountId,
principalTable: "Accounts",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_Addresses_Contacts_ContactId",
column: x => x.ContactId,
principalTable: "Contacts",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "Assets",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
AssetCode = table.Column<string>(type: "nvarchar(max)", nullable: false),
Name = table.Column<string>(type: "nvarchar(max)", nullable: false),
Description = table.Column<string>(type: "nvarchar(max)", nullable: true),
AccountId = table.Column<int>(type: "int", nullable: true),
LocationId = table.Column<int>(type: "int", nullable: true),
IsActive = table.Column<bool>(type: "bit", nullable: false),
IsDeleted = table.Column<bool>(type: "bit", nullable: true),
createdby = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeleterUserId = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierUserId = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Assets", x => x.Id);
table.ForeignKey(
name: "FK_Assets_Accounts_AccountId",
column: x => x.AccountId,
principalTable: "Accounts",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_Assets_Locations_LocationId",
column: x => x.LocationId,
principalTable: "Locations",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "Employees",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
FirstName = table.Column<string>(type: "nvarchar(max)", nullable: false),
MiddleName = table.Column<string>(type: "nvarchar(max)", nullable: true),
LastName = table.Column<string>(type: "nvarchar(max)", nullable: false),
DepartmentId = table.Column<int>(type: "int", nullable: true),
TimeZone = table.Column<string>(type: "nvarchar(max)", nullable: true),
Gender = table.Column<int>(type: "int", nullable: true),
DateOfBirth = table.Column<DateTime>(type: "datetime2", nullable: true),
JobTitleId = table.Column<int>(type: "int", nullable: true),
EmployeeNumber = table.Column<string>(type: "nvarchar(max)", nullable: true),
HireDate = table.Column<DateTime>(type: "datetime2", nullable: true),
RegionId = table.Column<int>(type: "int", nullable: true),
CanChangeProviderNumber = table.Column<bool>(type: "bit", nullable: true),
About = table.Column<string>(type: "nvarchar(max)", nullable: true),
PermissionGroupId = table.Column<int>(type: "int", nullable: true),
UserName = table.Column<string>(type: "nvarchar(max)", nullable: true),
ConfirmUserName = table.Column<string>(type: "nvarchar(max)", nullable: true),
SMSAddress = table.Column<string>(type: "nvarchar(max)", nullable: true),
Website = table.Column<string>(type: "nvarchar(max)", nullable: true),
FacebookUrl = table.Column<string>(type: "nvarchar(max)", nullable: true),
LinkedInUrl = table.Column<string>(type: "nvarchar(max)", nullable: true),
TwitterUrl = table.Column<string>(type: "nvarchar(max)", nullable: true),
Status = table.Column<string>(type: "nvarchar(max)", nullable: true),
LastLogin = table.Column<DateTime>(type: "datetime2", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: true),
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
CreatedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierUserId = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Employees", x => x.Id);
table.ForeignKey(
name: "FK_Employees_Departments_DepartmentId",
column: x => x.DepartmentId,
principalTable: "Departments",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_Employees_JobTitles_JobTitleId",
column: x => x.JobTitleId,
principalTable: "JobTitles",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_Employees_Regions_RegionId",
column: x => x.RegionId,
principalTable: "Regions",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "PMSchedules",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Name = table.Column<string>(type: "nvarchar(max)", nullable: false),
Frequency = table.Column<string>(type: "nvarchar(max)", nullable: false),
StartDate = table.Column<DateTime>(type: "datetime2", nullable: false),
IsActive = table.Column<bool>(type: "bit", nullable: false),
AssetId = table.Column<int>(type: "int", nullable: true),
LocationId = table.Column<int>(type: "int", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: true),
createdby = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeleterUserId = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierUserId = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PMSchedules", x => x.Id);
table.ForeignKey(
name: "FK_PMSchedules_Assets_AssetId",
column: x => x.AssetId,
principalTable: "Assets",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_PMSchedules_Locations_LocationId",
column: x => x.LocationId,
principalTable: "Locations",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "EmployeeAddresses",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
EmployeeId = table.Column<int>(type: "int", nullable: false),
Street = table.Column<string>(type: "nvarchar(max)", nullable: true),
StreetLineTwo = table.Column<string>(type: "nvarchar(max)", nullable: true),
City = table.Column<string>(type: "nvarchar(max)", nullable: true),
Zip = table.Column<string>(type: "nvarchar(max)", nullable: true),
CountryCode = table.Column<string>(type: "nvarchar(max)", nullable: true),
StateCode = table.Column<string>(type: "nvarchar(max)", nullable: true),
CountyId = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_EmployeeAddresses", x => x.Id);
table.ForeignKey(
name: "FK_EmployeeAddresses_Employees_EmployeeId",
column: x => x.EmployeeId,
principalTable: "Employees",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "EmployeeEmails",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
EmployeeId = table.Column<int>(type: "int", nullable: false),
EmailTypeId = table.Column<int>(type: "int", nullable: true),
Address = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
IsPrimary = table.Column<bool>(type: "bit", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: true),
createdby = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeleterUserId = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierUserId = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_EmployeeEmails", x => x.Id);
table.ForeignKey(
name: "FK_EmployeeEmails_Employees_EmployeeId",
column: x => x.EmployeeId,
principalTable: "Employees",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "EmployeePhones",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
EmployeeId = table.Column<int>(type: "int", nullable: false),
PhoneTypeId = table.Column<int>(type: "int", nullable: true),
Number = table.Column<string>(type: "nvarchar(max)", nullable: true),
Extension = table.Column<string>(type: "nvarchar(max)", nullable: true),
IsPrimary = table.Column<bool>(type: "bit", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_EmployeePhones", x => x.Id);
table.ForeignKey(
name: "FK_EmployeePhones_Employees_EmployeeId",
column: x => x.EmployeeId,
principalTable: "Employees",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "FollowUps",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Reason = table.Column<string>(type: "nvarchar(max)", nullable: false),
ScheduleDate = table.Column<DateTime>(type: "datetime2", nullable: false),
ScheduleStartTime = table.Column<TimeSpan>(type: "time", nullable: false),
Status = table.Column<string>(type: "nvarchar(max)", nullable: false),
EmployeeId = table.Column<int>(type: "int", nullable: true),
WorkOrderId = table.Column<int>(type: "int", nullable: true),
AccountId = table.Column<int>(type: "int", nullable: true),
LocationId = table.Column<int>(type: "int", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: true),
createdby = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeleterUserId = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierUserId = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_FollowUps", x => x.Id);
table.ForeignKey(
name: "FK_FollowUps_Accounts_AccountId",
column: x => x.AccountId,
principalTable: "Accounts",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_FollowUps_Employees_EmployeeId",
column: x => x.EmployeeId,
principalTable: "Employees",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_FollowUps_Locations_LocationId",
column: x => x.LocationId,
principalTable: "Locations",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_FollowUps_workOrders_WorkOrderId",
column: x => x.WorkOrderId,
principalTable: "workOrders",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_Contacts_AccountId",
table: "Contacts",
column: "AccountId");
migrationBuilder.CreateIndex(
name: "IX_Contacts_LocationId",
table: "Contacts",
column: "LocationId");
migrationBuilder.CreateIndex(
name: "IX_Addresses_AccountId",
table: "Addresses",
column: "AccountId");
migrationBuilder.CreateIndex(
name: "IX_Addresses_ContactId",
table: "Addresses",
column: "ContactId");
migrationBuilder.CreateIndex(
name: "IX_Assets_AccountId",
table: "Assets",
column: "AccountId");
migrationBuilder.CreateIndex(
name: "IX_Assets_LocationId",
table: "Assets",
column: "LocationId");
migrationBuilder.CreateIndex(
name: "IX_ContactDetails_ContactId",
table: "ContactDetails",
column: "ContactId");
migrationBuilder.CreateIndex(
name: "IX_EmployeeAddresses_EmployeeId",
table: "EmployeeAddresses",
column: "EmployeeId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_EmployeeEmails_EmployeeId",
table: "EmployeeEmails",
column: "EmployeeId");
migrationBuilder.CreateIndex(
name: "IX_EmployeePhones_EmployeeId",
table: "EmployeePhones",
column: "EmployeeId");
migrationBuilder.CreateIndex(
name: "IX_Employees_DepartmentId",
table: "Employees",
column: "DepartmentId");
migrationBuilder.CreateIndex(
name: "IX_Employees_JobTitleId",
table: "Employees",
column: "JobTitleId");
migrationBuilder.CreateIndex(
name: "IX_Employees_RegionId",
table: "Employees",
column: "RegionId");
migrationBuilder.CreateIndex(
name: "IX_FollowUps_AccountId",
table: "FollowUps",
column: "AccountId");
migrationBuilder.CreateIndex(
name: "IX_FollowUps_EmployeeId",
table: "FollowUps",
column: "EmployeeId");
migrationBuilder.CreateIndex(
name: "IX_FollowUps_LocationId",
table: "FollowUps",
column: "LocationId");
migrationBuilder.CreateIndex(
name: "IX_FollowUps_WorkOrderId",
table: "FollowUps",
column: "WorkOrderId");
migrationBuilder.CreateIndex(
name: "IX_PMSchedules_AssetId",
table: "PMSchedules",
column: "AssetId");
migrationBuilder.CreateIndex(
name: "IX_PMSchedules_LocationId",
table: "PMSchedules",
column: "LocationId");
migrationBuilder.AddForeignKey(
name: "FK_Contacts_Accounts_AccountId",
table: "Contacts",
column: "AccountId",
principalTable: "Accounts",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
name: "FK_Contacts_Locations_LocationId",
table: "Contacts",
column: "LocationId",
principalTable: "Locations",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Contacts_Accounts_AccountId",
table: "Contacts");
migrationBuilder.DropForeignKey(
name: "FK_Contacts_Locations_LocationId",
table: "Contacts");
migrationBuilder.DropTable(
name: "Addresses");
migrationBuilder.DropTable(
name: "ContactDetails");
migrationBuilder.DropTable(
name: "EmployeeAddresses");
migrationBuilder.DropTable(
name: "EmployeeEmails");
migrationBuilder.DropTable(
name: "EmployeePhones");
migrationBuilder.DropTable(
name: "FollowUps");
migrationBuilder.DropTable(
name: "PMSchedules");
migrationBuilder.DropTable(
name: "Employees");
migrationBuilder.DropTable(
name: "Assets");
migrationBuilder.DropTable(
name: "Departments");
migrationBuilder.DropTable(
name: "JobTitles");
migrationBuilder.DropTable(
name: "Regions");
migrationBuilder.DropTable(
name: "Accounts");
migrationBuilder.DropIndex(
name: "IX_Contacts_AccountId",
table: "Contacts");
migrationBuilder.DropIndex(
name: "IX_Contacts_LocationId",
table: "Contacts");
migrationBuilder.DropColumn(
name: "AccountId",
table: "Contacts");
migrationBuilder.DropColumn(
name: "Address1",
table: "Contacts");
migrationBuilder.DropColumn(
name: "Address2",
table: "Contacts");
migrationBuilder.DropColumn(
name: "City",
table: "Contacts");
migrationBuilder.DropColumn(
name: "ContactType",
table: "Contacts");
migrationBuilder.DropColumn(
name: "FacebookUrl",
table: "Contacts");
migrationBuilder.DropColumn(
name: "FirstName",
table: "Contacts");
migrationBuilder.DropColumn(
name: "LastName",
table: "Contacts");
migrationBuilder.DropColumn(
name: "LinkedInUrl",
table: "Contacts");
migrationBuilder.DropColumn(
name: "LocationId",
table: "Contacts");
migrationBuilder.DropColumn(
name: "MiddleName",
table: "Contacts");
migrationBuilder.DropColumn(
name: "Owner",
table: "Contacts");
migrationBuilder.DropColumn(
name: "State",
table: "Contacts");
migrationBuilder.DropColumn(
name: "Title",
table: "Contacts");
migrationBuilder.DropColumn(
name: "TwitterUrl",
table: "Contacts");
migrationBuilder.RenameColumn(
name: "Zip",
table: "Contacts",
newName: "Name");
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "AspNetUserTokens",
type: "nvarchar(128)",
maxLength: 128,
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(450)");
migrationBuilder.AlterColumn<string>(
name: "LoginProvider",
table: "AspNetUserTokens",
type: "nvarchar(128)",
maxLength: 128,
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(450)");
migrationBuilder.AlterColumn<string>(
name: "ProviderKey",
table: "AspNetUserLogins",
type: "nvarchar(128)",
maxLength: 128,
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(450)");
migrationBuilder.AlterColumn<string>(
name: "LoginProvider",
table: "AspNetUserLogins",
type: "nvarchar(128)",
maxLength: 128,
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(450)");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,129 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.SeaHavenIndustries.Migrations
{
/// <inheritdoc />
public partial class AddLocationFields : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "AccountId",
table: "Locations",
type: "int",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Address",
table: "Locations",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "City",
table: "Locations",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Contact",
table: "Locations",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "ContactEmail",
table: "Locations",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Phone",
table: "Locations",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "State",
table: "Locations",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Status",
table: "Locations",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "ZipCode",
table: "Locations",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_Locations_AccountId",
table: "Locations",
column: "AccountId");
migrationBuilder.AddForeignKey(
name: "FK_Locations_Accounts_AccountId",
table: "Locations",
column: "AccountId",
principalTable: "Accounts",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Locations_Accounts_AccountId",
table: "Locations");
migrationBuilder.DropIndex(
name: "IX_Locations_AccountId",
table: "Locations");
migrationBuilder.DropColumn(
name: "AccountId",
table: "Locations");
migrationBuilder.DropColumn(
name: "Address",
table: "Locations");
migrationBuilder.DropColumn(
name: "City",
table: "Locations");
migrationBuilder.DropColumn(
name: "Contact",
table: "Locations");
migrationBuilder.DropColumn(
name: "ContactEmail",
table: "Locations");
migrationBuilder.DropColumn(
name: "Phone",
table: "Locations");
migrationBuilder.DropColumn(
name: "State",
table: "Locations");
migrationBuilder.DropColumn(
name: "Status",
table: "Locations");
migrationBuilder.DropColumn(
name: "ZipCode",
table: "Locations");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,129 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.SeaHavenIndustries.Migrations
{
/// <inheritdoc />
public partial class AddExternalSyncFields : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Building",
table: "workOrders",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Customer",
table: "workOrders",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "DateReported",
table: "workOrders",
type: "datetime2",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "ExternalWorkOrderId",
table: "workOrders",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "ScheduledStart",
table: "workOrders",
type: "datetime2",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Severity",
table: "workOrders",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "SiteCode",
table: "workOrders",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "SourceEmailS3Key",
table: "workOrders",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Commenter",
table: "Comments",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "ExternalCommentId",
table: "Comments",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "RecordType",
table: "Comments",
type: "nvarchar(max)",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Building",
table: "workOrders");
migrationBuilder.DropColumn(
name: "Customer",
table: "workOrders");
migrationBuilder.DropColumn(
name: "DateReported",
table: "workOrders");
migrationBuilder.DropColumn(
name: "ExternalWorkOrderId",
table: "workOrders");
migrationBuilder.DropColumn(
name: "ScheduledStart",
table: "workOrders");
migrationBuilder.DropColumn(
name: "Severity",
table: "workOrders");
migrationBuilder.DropColumn(
name: "SiteCode",
table: "workOrders");
migrationBuilder.DropColumn(
name: "SourceEmailS3Key",
table: "workOrders");
migrationBuilder.DropColumn(
name: "Commenter",
table: "Comments");
migrationBuilder.DropColumn(
name: "ExternalCommentId",
table: "Comments");
migrationBuilder.DropColumn(
name: "RecordType",
table: "Comments");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,73 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.SeaHavenIndustries.Migrations
{
/// <inheritdoc />
public partial class AddCommentTypeAndAuditLog : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "CommentType",
table: "Comments",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.CreateTable(
name: "WorkOrderAuditLogs",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
WorkOrderId = table.Column<int>(type: "int", nullable: false),
UserId = table.Column<string>(type: "nvarchar(450)", nullable: true),
FieldName = table.Column<string>(type: "nvarchar(max)", nullable: true),
OldValue = table.Column<string>(type: "nvarchar(max)", nullable: true),
NewValue = table.Column<string>(type: "nvarchar(max)", nullable: true),
Action = table.Column<string>(type: "nvarchar(max)", nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_WorkOrderAuditLogs", x => x.Id);
table.ForeignKey(
name: "FK_WorkOrderAuditLogs_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_WorkOrderAuditLogs_workOrders_WorkOrderId",
column: x => x.WorkOrderId,
principalTable: "workOrders",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_WorkOrderAuditLogs_UserId",
table: "WorkOrderAuditLogs",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_WorkOrderAuditLogs_WorkOrderId",
table: "WorkOrderAuditLogs",
column: "WorkOrderId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "WorkOrderAuditLogs");
migrationBuilder.DropColumn(
name: "CommentType",
table: "Comments");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.SeaHavenIndustries.Migrations
{
/// <inheritdoc />
public partial class AddInternalWONumber : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "InternalWONumber",
table: "workOrders",
type: "nvarchar(max)",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "InternalWONumber",
table: "workOrders");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,109 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.SeaHavenIndustries.Migrations
{
/// <inheritdoc />
public partial class AddDropdownOptionsAndWOFields : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "CompletedDate",
table: "workOrders",
type: "datetime2",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Problem",
table: "workOrders",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "ScheduledDate",
table: "workOrders",
type: "datetime2",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Source",
table: "workOrders",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "SubTrade",
table: "workOrders",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Trade",
table: "workOrders",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<decimal>(
name: "VendorNTE",
table: "workOrders",
type: "decimal(18,2)",
nullable: true);
migrationBuilder.CreateTable(
name: "DropdownOptions",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Category = table.Column<string>(type: "nvarchar(max)", nullable: false),
Value = table.Column<string>(type: "nvarchar(max)", nullable: false),
ParentValue = table.Column<string>(type: "nvarchar(max)", nullable: true),
SortOrder = table.Column<int>(type: "int", nullable: false),
IsActive = table.Column<bool>(type: "bit", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_DropdownOptions", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DropdownOptions");
migrationBuilder.DropColumn(
name: "CompletedDate",
table: "workOrders");
migrationBuilder.DropColumn(
name: "Problem",
table: "workOrders");
migrationBuilder.DropColumn(
name: "ScheduledDate",
table: "workOrders");
migrationBuilder.DropColumn(
name: "Source",
table: "workOrders");
migrationBuilder.DropColumn(
name: "SubTrade",
table: "workOrders");
migrationBuilder.DropColumn(
name: "Trade",
table: "workOrders");
migrationBuilder.DropColumn(
name: "VendorNTE",
table: "workOrders");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,104 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.SeaHavenIndustries.Migrations
{
/// <inheritdoc />
public partial class AddVendorAndDispatch : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Vendors",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
CompanyName = table.Column<string>(type: "nvarchar(max)", nullable: true),
ContactName = table.Column<string>(type: "nvarchar(max)", nullable: true),
Email = table.Column<string>(type: "nvarchar(max)", nullable: true),
Phone = table.Column<string>(type: "nvarchar(max)", nullable: true),
Address = table.Column<string>(type: "nvarchar(max)", nullable: true),
City = table.Column<string>(type: "nvarchar(max)", nullable: true),
State = table.Column<string>(type: "nvarchar(max)", nullable: true),
Zip = table.Column<string>(type: "nvarchar(max)", nullable: true),
TradeSpecialties = table.Column<string>(type: "nvarchar(max)", nullable: true),
IsActive = table.Column<bool>(type: "bit", nullable: false),
IsDeleted = table.Column<bool>(type: "bit", nullable: true),
createdby = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeleterUserId = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierUserId = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Vendors", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Dispatches",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
WorkOrderId = table.Column<int>(type: "int", nullable: false),
VendorId = table.Column<int>(type: "int", nullable: false),
PONumber = table.Column<string>(type: "nvarchar(max)", nullable: true),
NTEAmount = table.Column<decimal>(type: "decimal(18,2)", nullable: true),
Description = table.Column<string>(type: "nvarchar(max)", nullable: true),
Status = table.Column<string>(type: "nvarchar(max)", nullable: true),
DispatchedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
EmailSent = table.Column<bool>(type: "bit", nullable: false),
ReplyToAddress = table.Column<string>(type: "nvarchar(max)", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: true),
createdby = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeleterUserId = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierUserId = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Dispatches", x => x.Id);
table.ForeignKey(
name: "FK_Dispatches_Vendors_VendorId",
column: x => x.VendorId,
principalTable: "Vendors",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_Dispatches_workOrders_WorkOrderId",
column: x => x.WorkOrderId,
principalTable: "workOrders",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_Dispatches_VendorId",
table: "Dispatches",
column: "VendorId");
migrationBuilder.CreateIndex(
name: "IX_Dispatches_WorkOrderId",
table: "Dispatches",
column: "WorkOrderId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Dispatches");
migrationBuilder.DropTable(
name: "Vendors");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,29 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.SeaHavenIndustries.Migrations
{
/// <inheritdoc />
public partial class AddDispatchScheduledDate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "ScheduledDate",
table: "Dispatches",
type: "datetime2",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ScheduledDate",
table: "Dispatches");
}
}
}

View file

@ -0,0 +1,70 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.SeaHavenIndustries.Migrations
{
/// <inheritdoc />
public partial class AddDispatchNumberAndCommentDispatchId : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "CompletedDate",
table: "Dispatches",
type: "datetime2",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "DispatchNumber",
table: "Dispatches",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "DispatchId",
table: "Comments",
type: "int",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_Comments_DispatchId",
table: "Comments",
column: "DispatchId");
migrationBuilder.AddForeignKey(
name: "FK_Comments_Dispatches_DispatchId",
table: "Comments",
column: "DispatchId",
principalTable: "Dispatches",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Comments_Dispatches_DispatchId",
table: "Comments");
migrationBuilder.DropIndex(
name: "IX_Comments_DispatchId",
table: "Comments");
migrationBuilder.DropColumn(
name: "CompletedDate",
table: "Dispatches");
migrationBuilder.DropColumn(
name: "DispatchNumber",
table: "Dispatches");
migrationBuilder.DropColumn(
name: "DispatchId",
table: "Comments");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,80 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.SeaHavenIndustries.Migrations
{
/// <inheritdoc />
public partial class AddTaskListTemplates : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "TaskListTemplates",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Name = table.Column<string>(type: "nvarchar(max)", nullable: true),
Description = table.Column<string>(type: "nvarchar(max)", nullable: true),
IsActive = table.Column<bool>(type: "bit", nullable: false),
IsDeleted = table.Column<bool>(type: "bit", nullable: true),
createdby = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeleterUserId = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierUserId = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_TaskListTemplates", x => x.Id);
});
migrationBuilder.CreateTable(
name: "TaskListTemplateItems",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
TaskListTemplateId = table.Column<int>(type: "int", nullable: false),
ItemText = table.Column<string>(type: "nvarchar(max)", nullable: true),
SortOrder = table.Column<int>(type: "int", nullable: false),
IsDeleted = table.Column<bool>(type: "bit", nullable: true),
createdby = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeleterUserId = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierUserId = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_TaskListTemplateItems", x => x.Id);
table.ForeignKey(
name: "FK_TaskListTemplateItems_TaskListTemplates_TaskListTemplateId",
column: x => x.TaskListTemplateId,
principalTable: "TaskListTemplates",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_TaskListTemplateItems_TaskListTemplateId",
table: "TaskListTemplateItems",
column: "TaskListTemplateId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "TaskListTemplateItems");
migrationBuilder.DropTable(
name: "TaskListTemplates");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,96 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.SeaHavenIndustries.Migrations
{
/// <inheritdoc />
public partial class MultiWODispatch : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<int>(
name: "WorkOrderId",
table: "Dispatches",
type: "int",
nullable: true,
oldClrType: typeof(int),
oldType: "int");
migrationBuilder.AddColumn<string>(
name: "AcceptToken",
table: "Dispatches",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "AcknowledgedAt",
table: "Dispatches",
type: "datetime2",
nullable: true);
migrationBuilder.CreateTable(
name: "DispatchWorkOrders",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
DispatchId = table.Column<int>(type: "int", nullable: false),
WorkOrderId = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_DispatchWorkOrders", x => x.Id);
table.ForeignKey(
name: "FK_DispatchWorkOrders_Dispatches_DispatchId",
column: x => x.DispatchId,
principalTable: "Dispatches",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_DispatchWorkOrders_workOrders_WorkOrderId",
column: x => x.WorkOrderId,
principalTable: "workOrders",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_DispatchWorkOrders_DispatchId",
table: "DispatchWorkOrders",
column: "DispatchId");
migrationBuilder.CreateIndex(
name: "IX_DispatchWorkOrders_WorkOrderId",
table: "DispatchWorkOrders",
column: "WorkOrderId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DispatchWorkOrders");
migrationBuilder.DropColumn(
name: "AcceptToken",
table: "Dispatches");
migrationBuilder.DropColumn(
name: "AcknowledgedAt",
table: "Dispatches");
migrationBuilder.AlterColumn<int>(
name: "WorkOrderId",
table: "Dispatches",
type: "int",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "int",
oldNullable: true);
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,70 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.SeaHavenIndustries.Migrations
{
/// <inheritdoc />
public partial class AddDispatchChecklist : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "DispatchChecklistItems",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
DispatchId = table.Column<int>(type: "int", nullable: false),
WorkOrderId = table.Column<int>(type: "int", nullable: true),
ItemText = table.Column<string>(type: "nvarchar(max)", nullable: true),
IsCompleted = table.Column<bool>(type: "bit", nullable: false),
CompletedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
CompletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
SortOrder = table.Column<int>(type: "int", nullable: false),
IsDeleted = table.Column<bool>(type: "bit", nullable: true),
createdby = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeleterUserId = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierUserId = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_DispatchChecklistItems", x => x.Id);
table.ForeignKey(
name: "FK_DispatchChecklistItems_Dispatches_DispatchId",
column: x => x.DispatchId,
principalTable: "Dispatches",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_DispatchChecklistItems_workOrders_WorkOrderId",
column: x => x.WorkOrderId,
principalTable: "workOrders",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_DispatchChecklistItems_DispatchId",
table: "DispatchChecklistItems",
column: "DispatchId");
migrationBuilder.CreateIndex(
name: "IX_DispatchChecklistItems_WorkOrderId",
table: "DispatchChecklistItems",
column: "WorkOrderId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DispatchChecklistItems");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,58 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.SeaHavenIndustries.Migrations
{
/// <inheritdoc />
public partial class AddDispatchSignoffs : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "DispatchSignoffs",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
DispatchId = table.Column<int>(type: "int", nullable: false),
SignoffType = table.Column<string>(type: "nvarchar(max)", nullable: true),
Name = table.Column<string>(type: "nvarchar(max)", nullable: true),
Signature = table.Column<string>(type: "nvarchar(max)", nullable: true),
SignatureMethod = table.Column<string>(type: "nvarchar(max)", nullable: true),
SignedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: true),
createdby = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeleterUserId = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierUserId = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_DispatchSignoffs", x => x.Id);
table.ForeignKey(
name: "FK_DispatchSignoffs_Dispatches_DispatchId",
column: x => x.DispatchId,
principalTable: "Dispatches",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_DispatchSignoffs_DispatchId",
table: "DispatchSignoffs",
column: "DispatchId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DispatchSignoffs");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,39 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.SeaHavenIndustries.Migrations
{
/// <inheritdoc />
public partial class AddDispatchVerification : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "VerifiedAt",
table: "Dispatches",
type: "datetime2",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "VerifiedBy",
table: "Dispatches",
type: "nvarchar(max)",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "VerifiedAt",
table: "Dispatches");
migrationBuilder.DropColumn(
name: "VerifiedBy",
table: "Dispatches");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,64 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.SeaHavenIndustries.Migrations
{
/// <inheritdoc />
public partial class AddVendorAccessToken : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "VendorAccessTokens",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
VendorId = table.Column<int>(type: "int", nullable: false),
Token = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
IssuedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
ExpiresAt = table.Column<DateTime>(type: "datetime2", nullable: false),
RevokedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
LastUsedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: true),
createdby = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeleterUserId = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierUserId = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_VendorAccessTokens", x => x.Id);
table.ForeignKey(
name: "FK_VendorAccessTokens_Vendors_VendorId",
column: x => x.VendorId,
principalTable: "Vendors",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_VendorAccessTokens_Token",
table: "VendorAccessTokens",
column: "Token",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_VendorAccessTokens_VendorId",
table: "VendorAccessTokens",
column: "VendorId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "VendorAccessTokens");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,64 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.SeaHavenIndustries.Migrations
{
/// <inheritdoc />
public partial class AddDispatchUpliftRequest : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "DispatchUpliftRequests",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
DispatchId = table.Column<int>(type: "int", nullable: false),
CurrentNTE = table.Column<decimal>(type: "decimal(18,2)", nullable: true),
RequestedNTE = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
VendorReason = table.Column<string>(type: "nvarchar(max)", nullable: true),
Status = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
RequiredTier = table.Column<int>(type: "int", nullable: false),
RequestedByVendorName = table.Column<string>(type: "nvarchar(max)", nullable: true),
DecidedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
DecidedByUserId = table.Column<string>(type: "nvarchar(max)", nullable: true),
DecisionNote = table.Column<string>(type: "nvarchar(max)", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: true),
createdby = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeleterUserId = table.Column<string>(type: "nvarchar(max)", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierUserId = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_DispatchUpliftRequests", x => x.Id);
table.ForeignKey(
name: "FK_DispatchUpliftRequests_Dispatches_DispatchId",
column: x => x.DispatchId,
principalTable: "Dispatches",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_DispatchUpliftRequests_DispatchId_Status",
table: "DispatchUpliftRequests",
columns: new[] { "DispatchId", "Status" },
unique: true,
filter: "[Status] = 'Pending'");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DispatchUpliftRequests");
}
}
}

View file

@ -7,6 +7,10 @@ namespace Data.SeaHavenIndustries
public class Comments : FullAuditEntity
{
public string? Commenttext { get; set; }
public string? Commenter { get; set; }
public string? RecordType { get; set; }
public string? CommentType { get; set; }
public string? ExternalCommentId { get; set; }
public string? Documents { get; set; }
public string? UserId { get; set; }
[ForeignKey(nameof(UserId))]
@ -14,5 +18,8 @@ namespace Data.SeaHavenIndustries
public int? WorkerOrderId { get; set; }
[ForeignKey(nameof(WorkerOrderId))]
public virtual WorkOrder? WorkOrder { get; set; }
public int? DispatchId { get; set; }
[ForeignKey(nameof(DispatchId))]
public virtual Dispatch? Dispatch { get; set; }
}
}

View file

@ -0,0 +1,34 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Data.SeaHavenIndustries
{
public class Dispatch : FullAuditEntity
{
public int? WorkOrderId { get; set; }
[ForeignKey(nameof(WorkOrderId))]
public virtual WorkOrder? WorkOrder { get; set; }
public int VendorId { get; set; }
[ForeignKey(nameof(VendorId))]
public virtual Vendor? Vendor { get; set; }
public string? DispatchNumber { get; set; }
public string? PONumber { get; set; }
public decimal? NTEAmount { get; set; }
public string? Description { get; set; }
public string? Status { get; set; }
public DateTime? ScheduledDate { get; set; }
public DateTime? CompletedDate { get; set; }
public DateTime? DispatchedAt { get; set; }
public DateTime? AcknowledgedAt { get; set; }
public bool EmailSent { get; set; }
public string? ReplyToAddress { get; set; }
public string? AcceptToken { get; set; }
public string? VerifiedBy { get; set; }
public DateTime? VerifiedAt { get; set; }
public ICollection<Comments>? Comments { get; set; }
public ICollection<DispatchWorkOrder>? DispatchWorkOrders { get; set; }
public ICollection<DispatchChecklistItem>? ChecklistItems { get; set; }
public ICollection<DispatchSignoff>? Signoffs { get; set; }
}
}

View file

@ -0,0 +1,19 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Data.SeaHavenIndustries
{
public class DispatchChecklistItem : FullAuditEntity
{
public int DispatchId { get; set; }
[ForeignKey(nameof(DispatchId))]
public virtual Dispatch? Dispatch { get; set; }
public int? WorkOrderId { get; set; }
[ForeignKey(nameof(WorkOrderId))]
public virtual WorkOrder? WorkOrder { get; set; }
public string? ItemText { get; set; }
public bool IsCompleted { get; set; }
public string? CompletedBy { get; set; }
public DateTime? CompletedAt { get; set; }
public int SortOrder { get; set; }
}
}

View file

@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Data.SeaHavenIndustries
{
public class DispatchSignoff : FullAuditEntity
{
public int DispatchId { get; set; }
[ForeignKey(nameof(DispatchId))]
public virtual Dispatch? Dispatch { get; set; }
public string? SignoffType { get; set; }
public string? Name { get; set; }
public string? Signature { get; set; }
public string? SignatureMethod { get; set; }
public DateTime? SignedAt { get; set; }
}
}

View file

@ -0,0 +1,32 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Data.SeaHavenIndustries
{
public class DispatchUpliftRequest : FullAuditEntity
{
public int DispatchId { get; set; }
[ForeignKey(nameof(DispatchId))]
public virtual Dispatch? Dispatch { get; set; }
[Column(TypeName = "decimal(18,2)")]
public decimal? CurrentNTE { get; set; }
[Column(TypeName = "decimal(18,2)")]
public decimal RequestedNTE { get; set; }
public string? VendorReason { get; set; }
[Required]
[MaxLength(20)]
public string Status { get; set; } = "Pending";
public int RequiredTier { get; set; }
public string? RequestedByVendorName { get; set; }
public DateTime? DecidedAt { get; set; }
public string? DecidedByUserId { get; set; }
public string? DecisionNote { get; set; }
}
}

View file

@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Data.SeaHavenIndustries
{
public class DispatchWorkOrder
{
public int Id { get; set; }
public int DispatchId { get; set; }
[ForeignKey(nameof(DispatchId))]
public virtual Dispatch? Dispatch { get; set; }
public int WorkOrderId { get; set; }
[ForeignKey(nameof(WorkOrderId))]
public virtual WorkOrder? WorkOrder { get; set; }
}
}

View file

@ -0,0 +1,12 @@
namespace Data.SeaHavenIndustries
{
public class DropdownOption
{
public int Id { get; set; }
public string Category { get; set; } = "";
public string Value { get; set; } = "";
public string? ParentValue { get; set; }
public int SortOrder { get; set; }
public bool IsActive { get; set; } = true;
}
}

View file

@ -86,8 +86,9 @@ namespace Data.SeaHavenIndustries
public Employee Employee { get; set; } = null!;
}
public class EmployeeEmail : FullAuditEntity
public class EmployeeEmail
{
public int Id { get; set; }
public int EmployeeId { get; set; }
public Employee? Employee { get; set; }
@ -99,22 +100,28 @@ namespace Data.SeaHavenIndustries
public bool? IsPrimary { get; set; }
}
public class Department : FullAuditEntity
public class Department
{
public int Id { get; set; }
[MaxLength(200)]
public string Name { get; set; } = null!;
public ICollection<Employee>? Employees { get; set; }
}
public class JobTitle : FullAuditEntity
public class JobTitle
{
public int Id { get; set; }
[MaxLength(200)]
public string Name { get; set; } = null!;
public ICollection<Employee>? Employees { get; set; }
}
public class Region : FullAuditEntity
public class Region
{
public int Id { get; set; }
[MaxLength(200)]
public string Name { get; set; } = null!;
public ICollection<Employee>? Employees { get; set; }

View file

@ -6,12 +6,26 @@ namespace Data.SeaHavenIndustries
{
public class Locations : FullAuditEntity
{
public int Id { get; set; }
public string? Title { get; set; }
public string? Name { get; set; }
public string? Latitude { get; set; }
public string? Longitude { get; set; }
// Address fields - match actual database columns
public string? Address1 { get; set; }
public string? Address2 { get; set; }
public string? City { get; set; }
public string? State { get; set; }
public string? Zip { get; set; }
// Contact fields - match actual database columns
public string? PhoneNumber { get; set; }
public string? Email { get; set; }
public string? Status { get; set; }
// Navigation Properties
public ICollection<Template>? Templates { get; set; }
public ICollection<WorkOrder>? workOrders { get; set; }
}
}

View file

@ -0,0 +1,10 @@
namespace Data.SeaHavenIndustries
{
public class TaskListTemplate : FullAuditEntity
{
public string? Name { get; set; }
public string? Description { get; set; }
public bool IsActive { get; set; } = true;
public ICollection<TaskListTemplateItem>? Items { get; set; }
}
}

View file

@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Data.SeaHavenIndustries
{
public class TaskListTemplateItem : FullAuditEntity
{
public int TaskListTemplateId { get; set; }
[ForeignKey(nameof(TaskListTemplateId))]
public virtual TaskListTemplate? Template { get; set; }
public string? ItemText { get; set; }
public int SortOrder { get; set; }
}
}

View file

@ -0,0 +1,17 @@
namespace Data.SeaHavenIndustries
{
public class Vendor : FullAuditEntity
{
public string? CompanyName { get; set; }
public string? ContactName { get; set; }
public string? Email { get; set; }
public string? Phone { get; set; }
public string? Address { get; set; }
public string? City { get; set; }
public string? State { get; set; }
public string? Zip { get; set; }
public string? TradeSpecialties { get; set; }
public bool IsActive { get; set; } = true;
public ICollection<Dispatch>? Dispatches { get; set; }
}
}

View file

@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Data.SeaHavenIndustries
{
public class VendorAccessToken : FullAuditEntity
{
public int VendorId { get; set; }
[ForeignKey(nameof(VendorId))]
public virtual Vendor? Vendor { get; set; }
[Required]
[MaxLength(64)]
public string Token { get; set; } = string.Empty;
public DateTime IssuedAt { get; set; }
public DateTime ExpiresAt { get; set; }
public DateTime? RevokedAt { get; set; }
public DateTime? LastUsedAt { get; set; }
}
}

View file

@ -0,0 +1,20 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Data.SeaHavenIndustries
{
public class WorkOrderAuditLog
{
public int Id { get; set; }
public int WorkOrderId { get; set; }
[ForeignKey(nameof(WorkOrderId))]
public virtual WorkOrder? WorkOrder { get; set; }
public string? UserId { get; set; }
[ForeignKey(nameof(UserId))]
public virtual ApplicationUser? User { get; set; }
public string? FieldName { get; set; }
public string? OldValue { get; set; }
public string? NewValue { get; set; }
public string? Action { get; set; }
public DateTime CreatedAt { get; set; }
}
}

View file

@ -6,9 +6,25 @@ namespace Data.SeaHavenIndustries
{
public class WorkOrder : FullAuditEntity
{
public string? InternalWONumber { get; set; }
public string? ExternalWorkOrderId { get; set; }
public string? WorkerOrderNumber { get; set; }
public string? WorkerOrderTitle { get; set; }
public string? Description { get; set; }
public string? Customer { get; set; }
public string? SiteCode { get; set; }
public string? Building { get; set; }
public string? Severity { get; set; }
public DateTime? DateReported { get; set; }
public DateTime? ScheduledStart { get; set; }
public string? SourceEmailS3Key { get; set; }
public string? Problem { get; set; }
public string? Trade { get; set; }
public string? SubTrade { get; set; }
public decimal? VendorNTE { get; set; }
public DateTime? ScheduledDate { get; set; }
public DateTime? CompletedDate { get; set; }
public string? Source { get; set; }
public bool? istemplate { get; set; }
public string? PO { get; set; }
public string? TT { get; set; }
@ -40,7 +56,6 @@ namespace Data.SeaHavenIndustries
public ICollection<WorkOrderContacts>? WorkOrderContacts { get; set; }
public ICollection<WorkOrderAttachments>? workOrderAttachments { get; set; }
public ICollection<WorkOrderCategories>? WorkOrderCategories { get; set; }
public ICollection<Dispatch>? Dispatches { get; set; }
}
}

109
FIX_LOCATIONS_SCHEMA.sql Normal file
View file

@ -0,0 +1,109 @@
-- 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

@ -0,0 +1,100 @@
using Data.SeaHavenIndustries;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Interfaces;
namespace SeaHaven.DataServices.Implementation
{
public class AccountDataService : IAccountDataService
{
private readonly ApplicationDbContext _context;
public AccountDataService(ApplicationDbContext context)
{
_context = context;
}
public async Task<Accounts?> GetByIdAsync(int id)
{
return await _context.Accounts.FindAsync(id);
}
public async Task<Accounts?> GetByIdWithDetailsAsync(int id)
{
return await _context.Accounts
.Include(a => a.Contacts)
.Include(a => a.Addresses)
.FirstOrDefaultAsync(a => a.Id == id);
}
public async Task<IEnumerable<Accounts>> GetAllAsync()
{
return await _context.Accounts.ToListAsync();
}
public async Task<(IEnumerable<Accounts> Items, int TotalCount)> GetPagedAsync(
int page,
int pageSize,
string? search = null)
{
search ??= "";
var query = _context.Accounts
.Where(a => a.IsDeleted == false || a.IsDeleted == null);
if (!string.IsNullOrWhiteSpace(search))
{
var s = search.ToLower();
query = query.Where(a =>
(a.Name ?? "").ToLower().Contains(s) ||
(a.Email ?? "").ToLower().Contains(s) ||
(a.Phone ?? "").Contains(s) ||
(a.Website ?? "").ToLower().Contains(s)
);
}
var totalCount = await query.CountAsync();
var items = await query
.OrderByDescending(a => a.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return (items, totalCount);
}
public async Task<Accounts> AddAsync(Accounts account)
{
account.CreatedDate = DateTime.Now;
await _context.Accounts.AddAsync(account);
await _context.SaveChangesAsync();
return account;
}
public async Task UpdateAsync(Accounts account)
{
account.LastModificationTime = DateTime.Now;
_context.Accounts.Update(account);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var entity = await GetByIdAsync(id);
if (entity != null)
{
_context.Accounts.Remove(entity);
await _context.SaveChangesAsync();
}
}
public async Task<bool> ExistsAsync(int id)
{
return await _context.Accounts.AnyAsync(a => a.Id == id);
}
public async Task<int> CountAsync()
{
return await _context.Accounts.CountAsync();
}
}
}

View file

@ -0,0 +1,108 @@
using Data.SeaHavenIndustries;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Interfaces;
namespace SeaHaven.DataServices.Implementation
{
public class AssetDataService : IAssetDataService
{
private readonly ApplicationDbContext _context;
public AssetDataService(ApplicationDbContext context)
{
_context = context;
}
public async Task<Assets?> GetByIdAsync(int id)
{
return await _context.Assets.FindAsync(id);
}
public async Task<Assets?> GetByIdWithDetailsAsync(int id)
{
return await _context.Assets
.Include(a => a.Account)
.Include(a => a.Location)
.FirstOrDefaultAsync(a => a.Id == id);
}
public async Task<IEnumerable<Assets>> GetAllAsync()
{
return await _context.Assets.ToListAsync();
}
public async Task<IEnumerable<Assets>> GetByLocationIdAsync(int locationId)
{
return await _context.Assets
.Where(a => a.LocationId == locationId)
.ToListAsync();
}
public async Task<(IEnumerable<Assets> Items, int TotalCount)> GetPagedAsync(
int page,
int pageSize,
string? search = null)
{
search ??= "";
var query = _context.Assets
.Include(a => a.Account)
.Include(a => a.Location)
.AsQueryable();
if (!string.IsNullOrWhiteSpace(search))
{
var s = search.ToLower();
query = query.Where(a =>
(a.Name != null && a.Name.ToLower().Contains(s)) ||
(a.AssetCode != null && a.AssetCode.ToLower().Contains(s)) ||
(a.Description != null && a.Description.ToLower().Contains(s))
);
}
var totalCount = await query.CountAsync();
var items = await query
.OrderByDescending(a => a.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return (items, totalCount);
}
public async Task<Assets> AddAsync(Assets asset)
{
asset.CreatedDate = DateTime.Now;
await _context.Assets.AddAsync(asset);
await _context.SaveChangesAsync();
return asset;
}
public async Task UpdateAsync(Assets asset)
{
asset.LastModificationTime = DateTime.Now;
_context.Assets.Update(asset);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var entity = await GetByIdAsync(id);
if (entity != null)
{
_context.Assets.Remove(entity);
await _context.SaveChangesAsync();
}
}
public async Task<bool> ExistsAsync(int id)
{
return await _context.Assets.AnyAsync(a => a.Id == id);
}
public async Task<int> CountAsync()
{
return await _context.Assets.CountAsync();
}
}
}

View file

@ -0,0 +1,56 @@
using Data.SeaHavenIndustries;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Interfaces;
namespace SeaHaven.DataServices.Implementation
{
public class CategoryDataService : ICategoryDataService
{
private readonly ApplicationDbContext _context;
public CategoryDataService(ApplicationDbContext context)
{
_context = context;
}
public async Task<Category?> GetByIdAsync(int id)
{
return await _context.Categories.FindAsync(id);
}
public async Task<IEnumerable<Category>> GetAllAsync()
{
return await _context.Categories.ToListAsync();
}
public async Task<Category> AddAsync(Category category)
{
category.CreatedDate = DateTime.Now;
await _context.Categories.AddAsync(category);
await _context.SaveChangesAsync();
return category;
}
public async Task UpdateAsync(Category category)
{
category.LastModificationTime = DateTime.Now;
_context.Categories.Update(category);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var entity = await GetByIdAsync(id);
if (entity != null)
{
_context.Categories.Remove(entity);
await _context.SaveChangesAsync();
}
}
public async Task<bool> ExistsAsync(int id)
{
return await _context.Categories.AnyAsync(c => c.Id == id);
}
}
}

View file

@ -0,0 +1,160 @@
using Data.SeaHavenIndustries;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Interfaces;
namespace SeaHaven.DataServices.Implementation
{
public class ContactDataService : IContactDataService
{
private readonly ApplicationDbContext _context;
public ContactDataService(ApplicationDbContext context)
{
_context = context;
}
public async Task<Contacts?> GetByIdAsync(int id)
{
return await _context.Contacts.FindAsync(id);
}
public async Task<Contacts?> GetByIdWithDetailsAsync(int id)
{
return await _context.Contacts
.Include(c => c.Location)
.Include(c => c.Account)
.FirstOrDefaultAsync(c => c.Id == id);
}
public async Task<IEnumerable<Contacts>> GetAllAsync()
{
return await _context.Contacts.ToListAsync();
}
public async Task<IEnumerable<Contacts>> GetByLocationIdAsync(int locationId)
{
return await _context.Contacts
.Where(c => c.LocationId == locationId)
.ToListAsync();
}
public async Task<IEnumerable<Contacts>> GetByAccountIdAsync(int accountId)
{
return await _context.Contacts
.Where(c => c.AccountId == accountId)
.ToListAsync();
}
public async Task<IEnumerable<Contacts>> GetByTypeAsync(string type)
{
return await _context.Contacts
.Where(c => c.ContactType == type)
.ToListAsync();
}
public async Task<Contacts?> GetByEmailAsync(string email)
{
return await _context.Contacts
.FirstOrDefaultAsync(c => c.Email == email);
}
public async Task<Contacts?> GetByPhoneAsync(string phone)
{
return await _context.Contacts
.FirstOrDefaultAsync(c => c.PhoneNumber == phone);
}
public async Task<(IEnumerable<Contacts> Items, int TotalCount)> GetPagedAsync(
int page,
int pageSize,
string? search = null,
string? type = null)
{
search ??= "";
var query = _context.Contacts.AsQueryable();
if (!string.IsNullOrWhiteSpace(search))
{
var s = search.ToLower();
query = query.Where(c =>
(c.FirstName != null && c.FirstName.ToLower().Contains(s)) ||
(c.LastName != null && c.LastName.ToLower().Contains(s)) ||
(c.Email != null && c.Email.ToLower().Contains(s)) ||
(c.PhoneNumber != null && c.PhoneNumber.Contains(s)));
}
if (!string.IsNullOrWhiteSpace(type))
{
query = query.Where(c => c.ContactType == type);
}
var totalCount = await query.CountAsync();
var items = await query
.OrderByDescending(c => c.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return (items, totalCount);
}
public async Task<Contacts> AddAsync(Contacts contact)
{
contact.CreatedDate = DateTime.Now;
await _context.Contacts.AddAsync(contact);
await _context.SaveChangesAsync();
return contact;
}
public async Task UpdateAsync(Contacts contact)
{
contact.LastModificationTime = DateTime.Now;
_context.Contacts.Update(contact);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var entity = await GetByIdAsync(id);
if (entity != null)
{
_context.Contacts.Remove(entity);
await _context.SaveChangesAsync();
}
}
public async Task<bool> ExistsAsync(int id)
{
return await _context.Contacts.AnyAsync(c => c.Id == id);
}
public async Task<bool> EmailExistsAsync(string email, int? excludeId = null)
{
return await _context.Contacts.AnyAsync(c =>
c.Email == email &&
(excludeId == null || c.Id != excludeId));
}
public async Task<bool> PhoneExistsAsync(string phone, int? excludeId = null)
{
return await _context.Contacts.AnyAsync(c =>
c.PhoneNumber == phone &&
(excludeId == null || c.Id != excludeId));
}
public async Task<int> CountAsync()
{
return await _context.Contacts.CountAsync();
}
public async Task<int> CountByAccountIdAsync(int accountId)
{
return await _context.Contacts.CountAsync(c => c.AccountId == accountId);
}
public async Task<int> CountByTypeAsync(string type)
{
return await _context.Contacts.CountAsync(c => c.ContactType == type);
}
}
}

Some files were not shown because too many files have changed in this diff Show more