mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 07:13:12 +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 Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Api.SeaHavenIndustries.Controllers
|
||||
{
|
||||
|
|
@ -17,11 +18,15 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
|
||||
private readonly ApplicationDbContext _db;
|
||||
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;
|
||||
_tokens = tokens;
|
||||
_config = config;
|
||||
_sendMessage = sendMessage;
|
||||
}
|
||||
|
||||
[HttpGet("session")]
|
||||
|
|
@ -103,6 +108,44 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
.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"
|
||||
|
|
@ -196,7 +239,8 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
},
|
||||
Checklist = checklist,
|
||||
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()
|
||||
{
|
||||
if (!Request.Headers.TryGetValue(TokenHeader, out var values)) return null;
|
||||
|
|
@ -487,5 +686,11 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
{
|
||||
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"))
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -19,5 +19,10 @@
|
|||
"FrontendBaseUrl": "http://localhost:3000",
|
||||
"VendorPortal": {
|
||||
"TokenLifetimeDays": 365
|
||||
},
|
||||
"Approvals": {
|
||||
"UpliftTier1MaxUsd": 2500,
|
||||
"Tier1Roles": [ "Admin", "Dispatcher" ],
|
||||
"Tier2Roles": [ "Admin", "Manager" ]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,5 +16,10 @@
|
|||
"ValidAudience": "http://localhost:4200",
|
||||
"ValidIssuer": "http://localhost:7195",
|
||||
"Secret": ""
|
||||
},
|
||||
"Approvals": {
|
||||
"UpliftTier1MaxUsd": 2500,
|
||||
"Tier1Roles": [ "Admin", "Dispatcher" ],
|
||||
"Tier2Roles": [ "Admin", "Manager" ]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,11 @@ namespace Data.SeaHavenIndustries
|
|||
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; }
|
||||
|
|
@ -49,6 +54,7 @@ namespace Data.SeaHavenIndustries
|
|||
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; }
|
||||
|
||||
|
|
|
|||
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");
|
||||
});
|
||||
|
||||
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 =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
|
@ -2424,6 +2494,17 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
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 =>
|
||||
{
|
||||
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