mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 09:33:13 +00:00
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.
This commit is contained in:
parent
b36ccd2758
commit
899ee7b83d
11 changed files with 3950 additions and 2 deletions
345
Api.SeaHavenIndustries/Controllers/DispatchController.cs
Normal file
345
Api.SeaHavenIndustries/Controllers/DispatchController.cs
Normal file
|
|
@ -0,0 +1,345 @@
|
||||||
|
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/dispatches")]
|
||||||
|
public class DispatchController : Controller
|
||||||
|
{
|
||||||
|
private readonly ApplicationDbContext _db;
|
||||||
|
|
||||||
|
public DispatchController(ApplicationDbContext db)
|
||||||
|
{
|
||||||
|
_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.Address,
|
||||||
|
dispatch.WorkOrder.Locations.City,
|
||||||
|
dispatch.WorkOrder.Locations.State,
|
||||||
|
dispatch.WorkOrder.Locations.ZipCode
|
||||||
|
},
|
||||||
|
WorkOrders = linkedWOs,
|
||||||
|
Checklist = checklist,
|
||||||
|
Signoffs = signoffs,
|
||||||
|
Comments = comments,
|
||||||
|
UpliftRequests = upliftRequests,
|
||||||
|
AuditLog = auditLog
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
241
Api.SeaHavenIndustries/Controllers/UpliftController.cs
Normal file
241
Api.SeaHavenIndustries/Controllers/UpliftController.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ using Api.SeaHavenIndustries.Helper;
|
||||||
using Data.SeaHavenIndustries;
|
using Data.SeaHavenIndustries;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
namespace Api.SeaHavenIndustries.Controllers
|
namespace Api.SeaHavenIndustries.Controllers
|
||||||
{
|
{
|
||||||
|
|
@ -17,11 +18,15 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
|
|
||||||
private readonly ApplicationDbContext _db;
|
private readonly ApplicationDbContext _db;
|
||||||
private readonly VendorPortalTokenService _tokens;
|
private readonly VendorPortalTokenService _tokens;
|
||||||
|
private readonly IConfiguration _config;
|
||||||
|
private readonly SendMessage _sendMessage;
|
||||||
|
|
||||||
public VendorPortalController(ApplicationDbContext db, VendorPortalTokenService tokens)
|
public VendorPortalController(ApplicationDbContext db, VendorPortalTokenService tokens, IConfiguration config, SendMessage sendMessage)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
_tokens = tokens;
|
_tokens = tokens;
|
||||||
|
_config = config;
|
||||||
|
_sendMessage = sendMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("session")]
|
[HttpGet("session")]
|
||||||
|
|
@ -103,6 +108,44 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
.Select(s => new { s.Id, s.SignoffType, s.Name, s.SignatureMethod, s.SignedAt })
|
.Select(s => new { s.Id, s.SignoffType, s.Name, s.SignatureMethod, s.SignedAt })
|
||||||
.ToListAsync();
|
.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
|
var rawComments = await (from c in _db.Comments
|
||||||
where c.DispatchId == id
|
where c.DispatchId == id
|
||||||
&& c.CommentType != "internal"
|
&& c.CommentType != "internal"
|
||||||
|
|
@ -196,7 +239,8 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
},
|
},
|
||||||
Checklist = checklist,
|
Checklist = checklist,
|
||||||
Signoffs = signoffs,
|
Signoffs = signoffs,
|
||||||
Comments = comments
|
Comments = comments,
|
||||||
|
UpliftRequests = upliftRequests
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -437,6 +481,161 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[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()
|
private async Task<Vendor?> ResolveVendorAsync()
|
||||||
{
|
{
|
||||||
if (!Request.Headers.TryGetValue(TokenHeader, out var values)) return null;
|
if (!Request.Headers.TryGetValue(TokenHeader, out var values)) return null;
|
||||||
|
|
@ -487,5 +686,11 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
{
|
{
|
||||||
public string? CommentText { get; set; }
|
public string? CommentText { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class UpliftRequestBody
|
||||||
|
{
|
||||||
|
public decimal RequestedNTE { get; set; }
|
||||||
|
public string? Reason { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,10 @@ if (app.Environment.IsDevelopment())
|
||||||
|
|
||||||
if (!await roleManager.RoleExistsAsync("Admin"))
|
if (!await roleManager.RoleExistsAsync("Admin"))
|
||||||
await roleManager.CreateAsync(new IdentityRole("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";
|
var adminEmail = "admin@seahavenind.com";
|
||||||
if (await userManager.FindByNameAsync(adminEmail) == null)
|
if (await userManager.FindByNameAsync(adminEmail) == null)
|
||||||
|
|
|
||||||
|
|
@ -19,5 +19,10 @@
|
||||||
"FrontendBaseUrl": "http://localhost:3000",
|
"FrontendBaseUrl": "http://localhost:3000",
|
||||||
"VendorPortal": {
|
"VendorPortal": {
|
||||||
"TokenLifetimeDays": 365
|
"TokenLifetimeDays": 365
|
||||||
|
},
|
||||||
|
"Approvals": {
|
||||||
|
"UpliftTier1MaxUsd": 2500,
|
||||||
|
"Tier1Roles": [ "Admin", "Dispatcher" ],
|
||||||
|
"Tier2Roles": [ "Admin", "Manager" ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,5 +16,10 @@
|
||||||
"ValidAudience": "http://localhost:4200",
|
"ValidAudience": "http://localhost:4200",
|
||||||
"ValidIssuer": "http://localhost:7195",
|
"ValidIssuer": "http://localhost:7195",
|
||||||
"Secret": ""
|
"Secret": ""
|
||||||
|
},
|
||||||
|
"Approvals": {
|
||||||
|
"UpliftTier1MaxUsd": 2500,
|
||||||
|
"Tier1Roles": [ "Admin", "Dispatcher" ],
|
||||||
|
"Tier2Roles": [ "Admin", "Manager" ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,11 @@ namespace Data.SeaHavenIndustries
|
||||||
builder.Entity<VendorAccessToken>()
|
builder.Entity<VendorAccessToken>()
|
||||||
.HasIndex(t => t.Token)
|
.HasIndex(t => t.Token)
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
|
builder.Entity<DispatchUpliftRequest>()
|
||||||
|
.HasIndex(u => new { u.DispatchId, u.Status })
|
||||||
|
.IsUnique()
|
||||||
|
.HasFilter("[Status] = 'Pending'");
|
||||||
}
|
}
|
||||||
public DbSet<Category> Categories { get; set; }
|
public DbSet<Category> Categories { get; set; }
|
||||||
public DbSet<Locations> Locations { get; set; }
|
public DbSet<Locations> Locations { get; set; }
|
||||||
|
|
@ -49,6 +54,7 @@ namespace Data.SeaHavenIndustries
|
||||||
public DbSet<DispatchWorkOrder> DispatchWorkOrders { get; set; }
|
public DbSet<DispatchWorkOrder> DispatchWorkOrders { get; set; }
|
||||||
public DbSet<DispatchChecklistItem> DispatchChecklistItems { get; set; }
|
public DbSet<DispatchChecklistItem> DispatchChecklistItems { get; set; }
|
||||||
public DbSet<DispatchSignoff> DispatchSignoffs { get; set; }
|
public DbSet<DispatchSignoff> DispatchSignoffs { get; set; }
|
||||||
|
public DbSet<DispatchUpliftRequest> DispatchUpliftRequests { get; set; }
|
||||||
public DbSet<TaskListTemplate> TaskListTemplates { get; set; }
|
public DbSet<TaskListTemplate> TaskListTemplates { get; set; }
|
||||||
public DbSet<TaskListTemplateItem> TaskListTemplateItems { get; set; }
|
public DbSet<TaskListTemplateItem> TaskListTemplateItems { get; set; }
|
||||||
|
|
||||||
|
|
|
||||||
2960
Data.SeaHavenIndustries/Migrations/20260420164708_AddDispatchUpliftRequest.Designer.cs
generated
Normal file
2960
Data.SeaHavenIndustries/Migrations/20260420164708_AddDispatchUpliftRequest.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -831,6 +831,76 @@ namespace Data.SeaHavenIndustries.Migrations
|
||||||
b.ToTable("DispatchSignoffs");
|
b.ToTable("DispatchSignoffs");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Data.SeaHavenIndustries.DispatchUpliftRequest", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime?>("CreatedDate")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<decimal?>("CurrentNTE")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DecidedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("DecidedByUserId")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("DecisionNote")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("DeleterUserId")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletionTime")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<int>("DispatchId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<bool?>("IsDeleted")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastModificationTime")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<int?>("LastModifierUserId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("RequestedByVendorName")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<decimal>("RequestedNTE")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<int>("RequiredTier")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("nvarchar(20)");
|
||||||
|
|
||||||
|
b.Property<string>("VendorReason")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("createdby")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("DispatchId", "Status")
|
||||||
|
.IsUnique()
|
||||||
|
.HasFilter("[Status] = 'Pending'");
|
||||||
|
|
||||||
|
b.ToTable("DispatchUpliftRequests");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Data.SeaHavenIndustries.DispatchWorkOrder", b =>
|
modelBuilder.Entity("Data.SeaHavenIndustries.DispatchWorkOrder", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
|
|
@ -2424,6 +2494,17 @@ namespace Data.SeaHavenIndustries.Migrations
|
||||||
b.Navigation("Dispatch");
|
b.Navigation("Dispatch");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Data.SeaHavenIndustries.DispatchUpliftRequest", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Data.SeaHavenIndustries.Dispatch", "Dispatch")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("DispatchId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Dispatch");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Data.SeaHavenIndustries.DispatchWorkOrder", b =>
|
modelBuilder.Entity("Data.SeaHavenIndustries.DispatchWorkOrder", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Data.SeaHavenIndustries.Dispatch", "Dispatch")
|
b.HasOne("Data.SeaHavenIndustries.Dispatch", "Dispatch")
|
||||||
|
|
|
||||||
32
Data.SeaHavenIndustries/Models/DispatchUpliftRequest.cs
Normal file
32
Data.SeaHavenIndustries/Models/DispatchUpliftRequest.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue