1812 lines
80 KiB
C#
1812 lines
80 KiB
C#
using Microsoft.AspNetCore.Authorization;
|
||
using Microsoft.AspNetCore.Identity;
|
||
using Microsoft.AspNetCore.Mvc;
|
||
using Microsoft.EntityFrameworkCore;
|
||
using PSTW_CentralSystem.Areas.IT.Models;
|
||
using PSTW_CentralSystem.DBContext;
|
||
using PSTW_CentralSystem.Models;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Threading.Tasks;
|
||
using PSTW_CentralSystem.Areas.IT.Printing;
|
||
using QuestPDF.Fluent;
|
||
using Org.BouncyCastle.Ocsp;
|
||
|
||
namespace PSTW_CentralSystem.Controllers.API
|
||
{
|
||
[ApiController]
|
||
[Route("[controller]")]
|
||
[Authorize] // keep your policy if needed
|
||
public class ItRequestAPI : ControllerBase
|
||
{
|
||
|
||
private static string Nz(string? s, string fallback = "") =>
|
||
string.IsNullOrWhiteSpace(s) ? fallback : s;
|
||
|
||
private readonly CentralSystemContext _db;
|
||
private readonly UserManager<UserModel> _userManager;
|
||
|
||
public ItRequestAPI(CentralSystemContext db, UserManager<UserModel> userManager)
|
||
{
|
||
_db = db;
|
||
_userManager = userManager;
|
||
}
|
||
|
||
// Helper for Edit window
|
||
private const int EDIT_WINDOW_HOURS_DEFAULT = 24; // <- change here anytime
|
||
private const int EDIT_WINDOW_HOURS_MIN = 1;
|
||
private const int EDIT_WINDOW_HOURS_MAX = 24 * 14; // clamp for safety (2 weeks)
|
||
|
||
// === Feature flags ===
|
||
// Toggle this to true if later wants Pending (no approvals yet) to be cancelable.
|
||
private const bool ALLOW_PENDING_CANCEL = false;
|
||
|
||
private static int ResolveEditWindowHours(HttpRequest req, int? fromDto)
|
||
{
|
||
// Priority: DTO -> Header -> Query -> Default
|
||
if (fromDto.HasValue) return Math.Clamp(fromDto.Value, EDIT_WINDOW_HOURS_MIN, EDIT_WINDOW_HOURS_MAX);
|
||
|
||
if (int.TryParse(req.Headers["X-Edit-Window-Hours"], out var h))
|
||
return Math.Clamp(h, EDIT_WINDOW_HOURS_MIN, EDIT_WINDOW_HOURS_MAX);
|
||
|
||
if (int.TryParse(req.Query["editWindowHours"], out var q))
|
||
return Math.Clamp(q, EDIT_WINDOW_HOURS_MIN, EDIT_WINDOW_HOURS_MAX);
|
||
|
||
return EDIT_WINDOW_HOURS_DEFAULT;
|
||
}
|
||
|
||
|
||
|
||
// Helper to get current user id (int)
|
||
private int GetCurrentUserIdOrThrow()
|
||
{
|
||
var userIdStr = _userManager.GetUserId(User);
|
||
if (string.IsNullOrEmpty(userIdStr) || !int.TryParse(userIdStr, out int currentUserId))
|
||
throw new UnauthorizedAccessException("Invalid user");
|
||
return currentUserId;
|
||
}
|
||
// --------------------------------------------------------------------
|
||
// GET: /ItRequestAPI/users?q=&take=100
|
||
// Returns light user list for dropdowns
|
||
// --------------------------------------------------------------------
|
||
[HttpGet("users")]
|
||
public async Task<IActionResult> GetUsers([FromQuery] string? q = null, [FromQuery] int take = 200)
|
||
{
|
||
take = Math.Clamp(take, 1, 500);
|
||
|
||
var users = _db.Users.AsQueryable();
|
||
|
||
if (!string.IsNullOrWhiteSpace(q))
|
||
{
|
||
q = q.Trim();
|
||
users = users.Where(u =>
|
||
(u.UserName ?? "").Contains(q) ||
|
||
(u.FullName ?? "").Contains(q) ||
|
||
(u.Email ?? "").Contains(q));
|
||
}
|
||
|
||
var data = await users
|
||
.OrderBy(u => u.Id)
|
||
.Take(take)
|
||
.Select(u => new
|
||
{
|
||
id = u.Id,
|
||
name = u.FullName ?? u.UserName,
|
||
userName = u.UserName,
|
||
email = u.Email
|
||
})
|
||
.ToListAsync();
|
||
|
||
return Ok(data);
|
||
}
|
||
// ===== Section B: Meta =====
|
||
[HttpGet("sectionB/meta")]
|
||
public async Task<IActionResult> SectionBMeta([FromQuery] int statusId)
|
||
{
|
||
int currentUserId;
|
||
try { currentUserId = GetCurrentUserIdOrThrow(); }
|
||
catch (Exception ex) { return Unauthorized(new { message = ex.Message }); }
|
||
|
||
var status = await _db.ItRequestStatus
|
||
.Include(s => s.Request)
|
||
.Include(s => s.Flow)
|
||
.FirstOrDefaultAsync(s => s.StatusId == statusId);
|
||
|
||
if (status == null)
|
||
return NotFound(new { message = "Status not found." });
|
||
|
||
if (status.Request == null)
|
||
return StatusCode(409, new { message = "This status row is orphaned (missing its request). Please re-create the request or fix the foreign key." });
|
||
|
||
var req = status.Request!;
|
||
var sectionB = await _db.ItRequestAssetInfo.FirstOrDefaultAsync(x => x.ItRequestId == status.ItRequestId);
|
||
|
||
bool isRequestor = req.UserId == currentUserId;
|
||
bool isItMember = await _db.ItTeamMembers.AnyAsync(t => t.UserId == currentUserId);
|
||
|
||
bool sectionBSent = sectionB?.SectionBSent == true;
|
||
bool locked = sectionBSent || (sectionB?.RequestorAccepted == true && sectionB?.ItAccepted == true);
|
||
|
||
return Ok(new
|
||
{
|
||
overallStatus = status.OverallStatus ?? "Pending",
|
||
requestorName = req.StaffName,
|
||
isRequestor,
|
||
isItMember,
|
||
sectionB = sectionB == null ? null : new
|
||
{
|
||
saved = true,
|
||
assetNo = sectionB.AssetNo ?? "",
|
||
machineId = sectionB.MachineId ?? "",
|
||
ipAddress = sectionB.IpAddress ?? "",
|
||
wiredMac = sectionB.WiredMac ?? "",
|
||
wifiMac = sectionB.WifiMac ?? "",
|
||
dialupAcc = sectionB.DialupAcc ?? "",
|
||
remarks = sectionB.Remarks ?? "",
|
||
lastEditedBy = sectionB.LastEditedByName ?? "",
|
||
lastEditedAt = sectionB.LastEditedAt
|
||
},
|
||
sectionBSent,
|
||
sectionBSentAt = sectionB?.SectionBSentAt,
|
||
locked,
|
||
requestorAccepted = sectionB?.RequestorAccepted ?? false,
|
||
requestorAcceptedAt = sectionB?.RequestorAcceptedAt,
|
||
itAccepted = sectionB?.ItAccepted ?? false,
|
||
itAcceptedAt = sectionB?.ItAcceptedAt,
|
||
itAcceptedBy = sectionB?.ItAcceptedByName
|
||
});
|
||
}
|
||
|
||
|
||
|
||
[HttpGet("sectionB/pdf")]
|
||
public async Task<IActionResult> SectionBPdf([FromQuery] int statusId)
|
||
{
|
||
var status = await _db.ItRequestStatus
|
||
.Include(s => s.Request).ThenInclude(r => r!.Hardware)
|
||
.Include(s => s.Request).ThenInclude(r => r!.Emails)
|
||
.Include(s => s.Request).ThenInclude(r => r!.OsRequirements)
|
||
.Include(s => s.Request).ThenInclude(r => r!.Software)
|
||
.Include(s => s.Request).ThenInclude(r => r!.SharedPermissions)
|
||
.Include(s => s.Flow)
|
||
.FirstOrDefaultAsync(s => s.StatusId == statusId);
|
||
|
||
if (status == null) return NotFound();
|
||
if (status.Request == null)
|
||
return StatusCode(409, new { message = "Missing Request for this status." });
|
||
|
||
var b = await _db.ItRequestAssetInfo
|
||
.SingleOrDefaultAsync(x => x.ItRequestId == status.ItRequestId);
|
||
|
||
string? hardwareJustification = null;
|
||
if (status.Request?.Hardware != null)
|
||
{
|
||
var firstDistinct = status.Request.Hardware
|
||
.Select(h => h.Justification?.Trim())
|
||
.Where(j => !string.IsNullOrWhiteSpace(j))
|
||
.Distinct(StringComparer.InvariantCultureIgnoreCase)
|
||
.FirstOrDefault();
|
||
|
||
hardwareJustification = firstDistinct;
|
||
}
|
||
|
||
|
||
if (status is null) return NotFound();
|
||
if (status.Request is null)
|
||
return StatusCode(409, new { message = "Missing Request for this status." });
|
||
|
||
var req = status.Request; // non-null after the guard
|
||
|
||
|
||
// Build the report model
|
||
var m = new ItRequestReportModel
|
||
{
|
||
ItRequestId = status.ItRequestId,
|
||
StatusId = status.StatusId,
|
||
OverallStatus = status.OverallStatus ?? "Pending",
|
||
SubmitDate = req.SubmitDate, // if nullable: req.SubmitDate ?? DateTime.MinValue
|
||
|
||
StaffName = req.StaffName ?? "",
|
||
CompanyName = req.CompanyName ?? "",
|
||
DepartmentName = req.DepartmentName ?? "",
|
||
Designation = req.Designation ?? "",
|
||
Location = req.Location ?? "",
|
||
EmploymentStatus = req.EmploymentStatus ?? "",
|
||
ContractEndDate = req.ContractEndDate,
|
||
RequiredDate = req.RequiredDate,
|
||
PhoneExt = req.PhoneExt ?? "",
|
||
|
||
// …keep the rest, but switch to 'req' for child lists too:
|
||
Hardware = req.Hardware?.Select(h =>
|
||
string.IsNullOrWhiteSpace(h.OtherDescription)
|
||
? (h.Category ?? "")
|
||
: $"{h.Category} – {h.OtherDescription}")
|
||
.Where(s => !string.IsNullOrWhiteSpace(s)).ToList() ?? new(),
|
||
|
||
Justification = hardwareJustification,
|
||
|
||
Emails = req.Emails?
|
||
.Select(e => e.ProposedAddress ?? "")
|
||
.Where(s => !string.IsNullOrWhiteSpace(s))
|
||
.ToList() ?? new(),
|
||
|
||
OsRequirements = req.OsRequirements?
|
||
.Select(o => o.RequirementText ?? "")
|
||
.Where(s => !string.IsNullOrWhiteSpace(s))
|
||
.ToList() ?? new(),
|
||
|
||
Software = req.Software?
|
||
.Select(sw => (sw.Bucket ?? "", sw.Name ?? "", sw.OtherName, sw.Notes))
|
||
.ToList() ?? new(),
|
||
|
||
SharedPerms = req.SharedPermissions?
|
||
.Select(sp => (sp.ShareName ?? "", sp.CanRead, sp.CanWrite, sp.CanDelete, sp.CanRemove))
|
||
.ToList() ?? new(),
|
||
|
||
AssetNo = b?.AssetNo ?? "",
|
||
MachineId = b?.MachineId ?? "",
|
||
IpAddress = b?.IpAddress ?? "",
|
||
WiredMac = b?.WiredMac ?? "",
|
||
WifiMac = b?.WifiMac ?? "",
|
||
DialupAcc = b?.DialupAcc ?? "",
|
||
Remarks = b?.Remarks ?? "",
|
||
RequestorName = req.StaffName ?? "",
|
||
RequestorAcceptedAt = b?.RequestorAcceptedAt,
|
||
ItCompletedBy = b?.ItAcceptedByName ?? "",
|
||
ItAcceptedAt = b?.ItAcceptedAt
|
||
};
|
||
|
||
|
||
if (status.Request?.Hardware != null)
|
||
{
|
||
foreach (var h in status.Request.Hardware)
|
||
{
|
||
var cat = (h.Category ?? "").Trim();
|
||
var purpose = (h.Purpose ?? "").Trim().ToLowerInvariant();
|
||
|
||
// Purposes (left column)
|
||
if (purpose == "newrecruitment" || purpose == "new recruitment" || purpose == "new")
|
||
m.HwPurposeNewRecruitment = true;
|
||
else if (purpose == "replacement")
|
||
m.HwPurposeReplacement = true;
|
||
else if (purpose == "additional")
|
||
m.HwPurposeAdditional = true;
|
||
|
||
// Categories (right column)
|
||
switch (cat)
|
||
{
|
||
case "DesktopAllIn": m.HwDesktopAllIn = true; break;
|
||
case "NotebookAllIn": m.HwNotebookAllIn = true; break;
|
||
case "DesktopOnly": m.HwDesktopOnly = true; break;
|
||
case "NotebookOnly": m.HwNotebookOnly = true; break;
|
||
case "NotebookBattery": m.HwNotebookBattery = true; break;
|
||
case "PowerAdapter": m.HwPowerAdapter = true; break;
|
||
case "Mouse": m.HwMouse = true; break;
|
||
case "ExternalHDD": m.HwExternalHdd = true; break;
|
||
case "Other":
|
||
if (!string.IsNullOrWhiteSpace(h.OtherDescription))
|
||
m.HwOtherText = h.OtherDescription!.Trim();
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---- Approver names + stage dates ------------------------------------
|
||
// Build a name map for all approver IDs (null-safe)
|
||
var approverIds = new int?[]
|
||
{
|
||
status.Flow?.HodUserId,
|
||
status.Flow?.GroupItHodUserId,
|
||
status.Flow?.FinHodUserId,
|
||
status.Flow?.MgmtUserId
|
||
};
|
||
|
||
var idSet = approverIds
|
||
.Where(i => i.HasValue).Select(i => i!.Value)
|
||
.Distinct().ToList();
|
||
|
||
var nameMap = await _db.Users
|
||
.Where(u => idSet.Contains(u.Id))
|
||
.ToDictionaryAsync(u => u.Id, u => u.FullName ?? u.UserName ?? $"User {u.Id}");
|
||
|
||
string Name(int? id) => id.HasValue ? nameMap.GetValueOrDefault(id.Value, "") : "";
|
||
|
||
// Push names into the report model for PDF
|
||
m.HodApprovedBy = Name(status.Flow?.HodUserId);
|
||
m.GitHodApprovedBy = Name(status.Flow?.GroupItHodUserId); // DB field: GroupItHodUserId
|
||
m.FinHodApprovedBy = Name(status.Flow?.FinHodUserId);
|
||
m.MgmtApprovedBy = Name(status.Flow?.MgmtUserId);
|
||
|
||
// Also expose the per-stage submit dates (your PDF uses them)
|
||
m.HodSubmitDate = status.HodSubmitDate;
|
||
m.GitHodSubmitDate = status.GitHodSubmitDate;
|
||
m.FinHodSubmitDate = status.FinHodSubmitDate;
|
||
m.MgmtSubmitDate = status.MgmtSubmitDate;
|
||
|
||
// ---- Gate: only after both acceptances -------------------------------
|
||
if (!(b?.RequestorAccepted == true && b?.ItAccepted == true))
|
||
return BadRequest(new { message = "PDF available after both acceptances." });
|
||
|
||
var pdf = new ItRequestPdfService().Generate(m);
|
||
var fileName = $"IT_Request_{m.ItRequestId}_SectionB.pdf";
|
||
return File(pdf, "application/pdf", fileName);
|
||
}
|
||
|
||
|
||
|
||
|
||
// ===== Section B: Save (IT team only, overall must be Approved) =====
|
||
public class SectionBSaveDto
|
||
{
|
||
public int StatusId { get; set; }
|
||
public string AssetNo { get; set; } = string.Empty;
|
||
public string MachineId { get; set; } = string.Empty;
|
||
public string IpAddress { get; set; } = string.Empty;
|
||
public string WiredMac { get; set; } = string.Empty;
|
||
public string WifiMac { get; set; } = string.Empty;
|
||
public string DialupAcc { get; set; } = string.Empty;
|
||
public string Remarks { get; set; } = string.Empty;
|
||
}
|
||
|
||
// REPLACE the old method with this one (same route name kept)
|
||
[HttpGet("sectionB/approvedList")]
|
||
public async Task<IActionResult> SectionBList([FromQuery] int month, [FromQuery] int year)
|
||
{
|
||
int currentUserId;
|
||
try { currentUserId = GetCurrentUserIdOrThrow(); }
|
||
catch (Exception ex) { return Unauthorized(new { message = ex.Message }); }
|
||
|
||
bool isItMember = await _db.ItTeamMembers.AnyAsync(t => t.UserId == currentUserId);
|
||
if (!isItMember) return Ok(new { isItMember = false, data = Array.Empty<object>() });
|
||
|
||
// Pull ALL requests for the month by SubmitDate (so "not eligible yet" also appear)
|
||
var baseQuery = _db.ItRequestStatus
|
||
.Include(s => s.Request)
|
||
.Where(s => s.Request != null &&
|
||
s.Request.SubmitDate.Month == month &&
|
||
s.Request.SubmitDate.Year == year)
|
||
.Select(s => new
|
||
{
|
||
S = s,
|
||
ApprovedAt = s.MgmtSubmitDate ?? s.FinHodSubmitDate ?? s.GitHodSubmitDate ?? s.HodSubmitDate
|
||
});
|
||
|
||
var rows = await baseQuery
|
||
.Select(x => new
|
||
{
|
||
x.S.StatusId,
|
||
x.S.ItRequestId,
|
||
StaffName = x.S.Request!.StaffName,
|
||
DepartmentName = x.S.Request.DepartmentName,
|
||
SubmitDate = x.S.Request.SubmitDate,
|
||
OverallStatus = x.S.OverallStatus ?? "Draft",
|
||
ApprovedAt = x.ApprovedAt,
|
||
|
||
B = _db.ItRequestAssetInfo
|
||
.Where(b => b.ItRequestId == x.S.ItRequestId)
|
||
.Select(b => new
|
||
{
|
||
saved = true,
|
||
b.SectionBSent,
|
||
b.RequestorAccepted,
|
||
b.ItAccepted,
|
||
b.RequestorAcceptedAt,
|
||
b.ItAcceptedAt,
|
||
b.ItAcceptedByName,
|
||
b.LastEditedAt,
|
||
b.LastEditedByName
|
||
})
|
||
.FirstOrDefault()
|
||
})
|
||
.ToListAsync();
|
||
|
||
// Stage derivation
|
||
string Stage(string overall, dynamic b)
|
||
{
|
||
bool isApproved = string.Equals(overall, "Approved", StringComparison.OrdinalIgnoreCase);
|
||
|
||
if (!isApproved) return "NOT_ELIGIBLE"; // first part not approved yet
|
||
if (b == null) return "PENDING"; // no Section B row yet
|
||
|
||
bool sent = b.SectionBSent == true;
|
||
bool req = b.RequestorAccepted == true;
|
||
bool it = b.ItAccepted == true;
|
||
|
||
if (req && it) return "COMPLETE"; // both accepted
|
||
if (!sent) return "DRAFT"; // saved but not sent
|
||
return "AWAITING"; // one side accepted
|
||
}
|
||
|
||
int Rank(string stage) => stage switch
|
||
{
|
||
"PENDING" => 0,
|
||
"DRAFT" => 1,
|
||
"AWAITING" => 2,
|
||
"COMPLETE" => 3,
|
||
"NOT_ELIGIBLE" => 4,
|
||
_ => 9
|
||
};
|
||
|
||
var data = rows
|
||
.Select(x =>
|
||
{
|
||
var stage = Stage(x.OverallStatus, x.B);
|
||
return new
|
||
{
|
||
statusId = x.StatusId,
|
||
itRequestId = x.ItRequestId,
|
||
staffName = x.StaffName,
|
||
departmentName = x.DepartmentName,
|
||
submitDate = x.SubmitDate,
|
||
approvedAt = x.ApprovedAt,
|
||
overallStatus = x.OverallStatus,
|
||
stage, // <-- used by UI badges/sorting
|
||
stageRank = Rank(stage), // <-- used by UI sorting
|
||
|
||
sb = new
|
||
{
|
||
saved = x.B != null,
|
||
requestorAccepted = x.B?.RequestorAccepted ?? false,
|
||
requestorAcceptedAt = x.B?.RequestorAcceptedAt,
|
||
itAccepted = x.B?.ItAccepted ?? false,
|
||
itAcceptedAt = x.B?.ItAcceptedAt,
|
||
itAcceptedBy = x.B?.ItAcceptedByName,
|
||
lastEditedAt = x.B?.LastEditedAt,
|
||
lastEditedBy = x.B?.LastEditedByName
|
||
}
|
||
};
|
||
})
|
||
.Where(x => x.stage != "NOT_ELIGIBLE")
|
||
.OrderBy(x => x.stageRank)
|
||
.ThenByDescending(x => x.approvedAt ?? x.submitDate)
|
||
.ToList();
|
||
|
||
return Ok(new { isItMember = true, data });
|
||
}
|
||
|
||
|
||
|
||
|
||
[HttpPost("sectionB/save")]
|
||
public async Task<IActionResult> SectionBSave([FromBody] SectionBSaveDto dto)
|
||
{
|
||
int currentUserId;
|
||
try { currentUserId = GetCurrentUserIdOrThrow(); } catch (Exception ex) { return Unauthorized(ex.Message); }
|
||
|
||
var status = await _db.ItRequestStatus
|
||
.Include(s => s.Request)
|
||
.FirstOrDefaultAsync(s => s.StatusId == dto.StatusId);
|
||
|
||
if (status == null) return NotFound("Status not found");
|
||
if ((status.OverallStatus ?? "Pending") != "Approved")
|
||
return BadRequest(new { message = "Section B is available only after OverallStatus is Approved." });
|
||
|
||
bool isItMember = await _db.ItTeamMembers.AnyAsync(t => t.UserId == currentUserId);
|
||
if (!isItMember) return Unauthorized("Only IT Team members can edit Section B.");
|
||
|
||
// basic server-side validation (at least one identifier)
|
||
List<string> errors = new();
|
||
if (string.IsNullOrWhiteSpace(dto.AssetNo) &&
|
||
string.IsNullOrWhiteSpace(dto.MachineId) &&
|
||
string.IsNullOrWhiteSpace(dto.IpAddress))
|
||
errors.Add("Provide at least one of: Asset No, Machine ID, or IP Address.");
|
||
if (errors.Count > 0) return BadRequest(new { message = string.Join(" ", errors) });
|
||
|
||
var existing = await _db.ItRequestAssetInfo.FirstOrDefaultAsync(x => x.ItRequestId == status.ItRequestId);
|
||
if (existing != null && existing.SectionBSent)
|
||
return BadRequest(new { message = "This Section B was sent. It can’t be edited anymore." });
|
||
|
||
var editor = await _db.Users.FirstOrDefaultAsync(u => u.Id == currentUserId);
|
||
var editorName = editor?.FullName ?? editor?.UserName ?? $"User {currentUserId}";
|
||
var now = DateTime.Now;
|
||
|
||
try
|
||
{
|
||
if (existing == null)
|
||
{
|
||
existing = new ItRequestAssetInfo
|
||
{
|
||
ItRequestId = status.ItRequestId,
|
||
AssetNo = dto.AssetNo?.Trim(),
|
||
MachineId = dto.MachineId?.Trim(),
|
||
IpAddress = dto.IpAddress?.Trim(),
|
||
WiredMac = dto.WiredMac?.Trim(),
|
||
WifiMac = dto.WifiMac?.Trim(),
|
||
DialupAcc = dto.DialupAcc?.Trim(),
|
||
Remarks = dto.Remarks?.Trim(),
|
||
LastEditedAt = now,
|
||
LastEditedByUserId = currentUserId,
|
||
LastEditedByName = editorName,
|
||
SectionBSent = false,
|
||
SectionBSentAt = null,
|
||
// ensure accept flags are clear on draft save
|
||
RequestorAccepted = false,
|
||
RequestorAcceptedAt = null,
|
||
ItAccepted = false,
|
||
ItAcceptedAt = null,
|
||
ItAcceptedByUserId = null,
|
||
ItAcceptedByName = null
|
||
};
|
||
_db.ItRequestAssetInfo.Add(existing);
|
||
}
|
||
else
|
||
{
|
||
existing.AssetNo = dto.AssetNo?.Trim();
|
||
existing.MachineId = dto.MachineId?.Trim();
|
||
existing.IpAddress = dto.IpAddress?.Trim();
|
||
existing.WiredMac = dto.WiredMac?.Trim();
|
||
existing.WifiMac = dto.WifiMac?.Trim();
|
||
existing.DialupAcc = dto.DialupAcc?.Trim();
|
||
existing.Remarks = dto.Remarks?.Trim();
|
||
existing.LastEditedAt = now;
|
||
existing.LastEditedByUserId = currentUserId;
|
||
existing.LastEditedByName = editorName;
|
||
|
||
// Saving draft always keeps it editable (not sent)
|
||
existing.SectionBSent = false;
|
||
existing.SectionBSentAt = null;
|
||
|
||
// if someone had previously accepted, a new draft save clears them
|
||
existing.RequestorAccepted = false;
|
||
existing.RequestorAcceptedAt = null;
|
||
existing.ItAccepted = false;
|
||
existing.ItAcceptedAt = null;
|
||
existing.ItAcceptedByUserId = null;
|
||
existing.ItAcceptedByName = null;
|
||
}
|
||
|
||
await _db.SaveChangesAsync();
|
||
return Ok(new { message = "Draft saved." });
|
||
}
|
||
catch (DbUpdateException dbx)
|
||
{
|
||
return StatusCode(400, new { message = "Validation/DB error while saving Section B.", detail = dbx.InnerException?.Message ?? dbx.Message });
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return StatusCode(500, new { message = "Server error while saving Section B.", detail = ex.Message });
|
||
}
|
||
}
|
||
|
||
public class SectionBSendDto
|
||
{
|
||
public int StatusId { get; set; }
|
||
public string? AssetNo { get; set; }
|
||
public string? MachineId { get; set; }
|
||
public string? IpAddress { get; set; }
|
||
public string? WiredMac { get; set; }
|
||
public string? WifiMac { get; set; }
|
||
public string? DialupAcc { get; set; }
|
||
public string? Remarks { get; set; }
|
||
}
|
||
|
||
|
||
[HttpPost("sectionB/send")]
|
||
public async Task<IActionResult> SectionBSend([FromBody] SectionBSendDto dto)
|
||
{
|
||
int currentUserId;
|
||
try { currentUserId = GetCurrentUserIdOrThrow(); } catch (Exception ex) { return Unauthorized(ex.Message); }
|
||
|
||
var status = await _db.ItRequestStatus
|
||
.Include(s => s.Request)
|
||
.FirstOrDefaultAsync(s => s.StatusId == dto.StatusId);
|
||
|
||
if (status == null) return NotFound(new { message = "Status not found" });
|
||
if ((status.OverallStatus ?? "Pending") != "Approved")
|
||
return BadRequest(new { message = "Section B can be sent only after OverallStatus is Approved." });
|
||
|
||
bool isItMember = await _db.ItTeamMembers.AnyAsync(t => t.UserId == currentUserId);
|
||
if (!isItMember) return Unauthorized(new { message = "Only IT can send Section B." });
|
||
|
||
var b = await _db.ItRequestAssetInfo.FirstOrDefaultAsync(x => x.ItRequestId == status.ItRequestId);
|
||
if (b?.SectionBSent == true)
|
||
return BadRequest(new { message = "Section B already sent." });
|
||
|
||
var editor = await _db.Users.FirstOrDefaultAsync(u => u.Id == currentUserId);
|
||
var editorName = editor?.FullName ?? editor?.UserName ?? $"User {currentUserId}";
|
||
var now = DateTime.Now;
|
||
|
||
if (b == null)
|
||
{
|
||
b = new ItRequestAssetInfo
|
||
{
|
||
ItRequestId = status.ItRequestId,
|
||
RequestorAccepted = false,
|
||
RequestorAcceptedAt = null,
|
||
ItAccepted = false,
|
||
ItAcceptedAt = null,
|
||
ItAcceptedByUserId = null,
|
||
ItAcceptedByName = null
|
||
};
|
||
_db.ItRequestAssetInfo.Add(b);
|
||
}
|
||
|
||
// Apply incoming fields (allow send without prior draft)
|
||
if (dto.AssetNo != null) b.AssetNo = dto.AssetNo.Trim();
|
||
if (dto.MachineId != null) b.MachineId = dto.MachineId.Trim();
|
||
if (dto.IpAddress != null) b.IpAddress = dto.IpAddress.Trim();
|
||
if (dto.WiredMac != null) b.WiredMac = dto.WiredMac.Trim();
|
||
if (dto.WifiMac != null) b.WifiMac = dto.WifiMac.Trim();
|
||
if (dto.DialupAcc != null) b.DialupAcc = dto.DialupAcc.Trim();
|
||
if (dto.Remarks != null) b.Remarks = dto.Remarks.Trim();
|
||
|
||
b.LastEditedAt = now;
|
||
b.LastEditedByUserId = currentUserId;
|
||
b.LastEditedByName = editorName;
|
||
|
||
// Require at least one identifier
|
||
if (string.IsNullOrWhiteSpace(b.AssetNo) &&
|
||
string.IsNullOrWhiteSpace(b.MachineId) &&
|
||
string.IsNullOrWhiteSpace(b.IpAddress))
|
||
return BadRequest(new { message = "Provide at least Asset No / Machine ID / IP Address before sending." });
|
||
|
||
b.SectionBSent = true;
|
||
b.SectionBSentAt = now;
|
||
|
||
await _db.SaveChangesAsync();
|
||
return Ok(new { message = "Section B sent. It is now locked for editing until both acceptances complete." });
|
||
}
|
||
|
||
|
||
public class SectionBResetDto { public int StatusId { get; set; } }
|
||
|
||
[HttpPost("sectionB/reset")]
|
||
public async Task<IActionResult> SectionBReset([FromBody] SectionBResetDto dto)
|
||
{
|
||
int currentUserId;
|
||
try { currentUserId = GetCurrentUserIdOrThrow(); } catch (Exception ex) { return Unauthorized(ex.Message); }
|
||
|
||
bool isItMember = await _db.ItTeamMembers.AnyAsync(t => t.UserId == currentUserId);
|
||
if (!isItMember) return Unauthorized(new { message = "Only IT can reset Section B." });
|
||
|
||
var status = await _db.ItRequestStatus
|
||
.Include(s => s.Request)
|
||
.FirstOrDefaultAsync(s => s.StatusId == dto.StatusId);
|
||
if (status == null) return NotFound(new { message = "Status not found" });
|
||
|
||
var b = await _db.ItRequestAssetInfo.FirstOrDefaultAsync(x => x.ItRequestId == status.ItRequestId);
|
||
if (b == null) return Ok(new { message = "Nothing to reset." });
|
||
|
||
if (b.SectionBSent)
|
||
return BadRequest(new { message = "Cannot reset after Section B was sent." });
|
||
|
||
b.AssetNo = b.MachineId = b.IpAddress = b.WiredMac = b.WifiMac = b.DialupAcc = b.Remarks = null;
|
||
b.RequestorAccepted = false; b.RequestorAcceptedAt = null;
|
||
b.ItAccepted = false; b.ItAcceptedAt = null; b.ItAcceptedByUserId = null; b.ItAcceptedByName = null;
|
||
b.LastEditedAt = DateTime.Now;
|
||
b.LastEditedByUserId = currentUserId;
|
||
b.LastEditedByName = (await _db.Users.FirstOrDefaultAsync(u => u.Id == currentUserId))?.FullName;
|
||
|
||
await _db.SaveChangesAsync();
|
||
return Ok(new { message = "Section B reset to empty draft." });
|
||
}
|
||
|
||
|
||
|
||
// ===== Section B: Accept (REQUESTOR or IT) =====
|
||
public class SectionBAcceptDto { public int StatusId { get; set; } public string By { get; set; } = string.Empty; } // REQUESTOR | IT
|
||
|
||
[HttpPost("sectionB/accept")]
|
||
public async Task<IActionResult> SectionBAccept([FromBody] SectionBAcceptDto dto)
|
||
{
|
||
int currentUserId;
|
||
try { currentUserId = GetCurrentUserIdOrThrow(); } catch (Exception ex) { return Unauthorized(ex.Message); }
|
||
|
||
var status = await _db.ItRequestStatus
|
||
.Include(s => s.Request)
|
||
.FirstOrDefaultAsync(s => s.StatusId == dto.StatusId);
|
||
if (status == null) return NotFound("Status not found");
|
||
|
||
var sectionB = await _db.ItRequestAssetInfo.FirstOrDefaultAsync(x => x.ItRequestId == status.ItRequestId);
|
||
if (sectionB == null) return BadRequest(new { message = "Save Section B before acceptance." });
|
||
|
||
if (!sectionB.SectionBSent)
|
||
return BadRequest(new { message = "Acceptances are only available after Section B is sent." });
|
||
|
||
var now = DateTime.Now;
|
||
var actor = await _db.Users.FirstOrDefaultAsync(u => u.Id == currentUserId);
|
||
var actorName = actor?.FullName ?? actor?.UserName ?? $"User {currentUserId}";
|
||
|
||
if (string.Equals(dto.By, "REQUESTOR", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
if (status.Request.UserId != currentUserId)
|
||
return Unauthorized("Only the requestor can perform this acceptance.");
|
||
if (sectionB.RequestorAccepted)
|
||
return BadRequest(new { message = "Requestor already accepted." });
|
||
|
||
sectionB.RequestorAccepted = true;
|
||
sectionB.RequestorAcceptedAt = now;
|
||
}
|
||
else if (string.Equals(dto.By, "IT", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
bool isItMember = await _db.ItTeamMembers.AnyAsync(t => t.UserId == currentUserId);
|
||
if (!isItMember) return Unauthorized("Only IT Team members can accept as IT.");
|
||
if (sectionB.ItAccepted)
|
||
return BadRequest(new { message = "IT already accepted." });
|
||
|
||
sectionB.ItAccepted = true;
|
||
sectionB.ItAcceptedAt = now;
|
||
sectionB.ItAcceptedByUserId = currentUserId;
|
||
sectionB.ItAcceptedByName = actorName;
|
||
}
|
||
else
|
||
{
|
||
return BadRequest(new { message = "Unknown acceptance type." });
|
||
}
|
||
|
||
await _db.SaveChangesAsync();
|
||
return Ok(new { message = "Accepted." });
|
||
}
|
||
|
||
|
||
// ===== IT Team maintenance (simple stub) =====
|
||
// GET: /ItRequestAPI/itTeam
|
||
[HttpGet("itTeam")]
|
||
public async Task<IActionResult> GetItTeam() =>
|
||
Ok(await _db.ItTeamMembers.Select(t => t.UserId).ToListAsync());
|
||
|
||
// POST: /ItRequestAPI/itTeam body: { userIds: [1,2,3] }
|
||
public class ItTeamDto { public List<int> UserIds { get; set; } = new(); }
|
||
[HttpPost("itTeam")]
|
||
public async Task<IActionResult> SetItTeam([FromBody] ItTeamDto dto)
|
||
{
|
||
// Add your own admin authorization guard here
|
||
var all = _db.ItTeamMembers;
|
||
_db.ItTeamMembers.RemoveRange(all);
|
||
await _db.SaveChangesAsync();
|
||
|
||
foreach (var uid in dto.UserIds.Distinct())
|
||
_db.ItTeamMembers.Add(new ItTeamMember { UserId = uid });
|
||
|
||
await _db.SaveChangesAsync();
|
||
return Ok(new { message = "IT Team updated." });
|
||
}
|
||
|
||
|
||
// --------------------------------------------------------------------
|
||
// GET: /ItRequestAPI/me
|
||
// Pull snapshot from aspnetusers -> departments -> companies
|
||
// Used by Create.cshtml to prefill read-only requester details
|
||
// --------------------------------------------------------------------
|
||
[HttpGet("me")]
|
||
public async Task<IActionResult> Me()
|
||
{
|
||
var userIdStr = _userManager.GetUserId(User);
|
||
if (string.IsNullOrEmpty(userIdStr) || !int.TryParse(userIdStr, out int currentUserId))
|
||
return Unauthorized("Invalid user");
|
||
|
||
var user = await _db.Users.FirstOrDefaultAsync(u => u.Id == currentUserId);
|
||
if (user == null) return NotFound("User not found");
|
||
|
||
var dept = await _db.Departments.FirstOrDefaultAsync(d => d.DepartmentId == user.departmentId);
|
||
var comp = dept != null
|
||
? await _db.Companies.FirstOrDefaultAsync(c => c.CompanyId == dept.CompanyId)
|
||
: null;
|
||
|
||
return Ok(new
|
||
{
|
||
userId = user.Id,
|
||
staffName = user.FullName ?? user.UserName,
|
||
departmentId = dept?.DepartmentId,
|
||
departmentName = dept?.DepartmentName ?? "",
|
||
companyId = comp?.CompanyId,
|
||
companyName = comp?.CompanyName ?? "",
|
||
designation = "", // adjust if you actually store this somewhere
|
||
location = "",
|
||
employmentStatus = "",
|
||
contractEndDate = (DateTime?)null,
|
||
phoneExt = user.PhoneNumber ?? ""
|
||
});
|
||
}
|
||
#region Approval razor
|
||
// --------------------------------------------------------------------
|
||
// GET: /ItRequestAPI/pending?month=9&year=2025
|
||
// --------------------------------------------------------------------
|
||
[HttpGet("pending")]
|
||
public async Task<IActionResult> GetPending(int month, int year)
|
||
{
|
||
var userIdStr = _userManager.GetUserId(User);
|
||
if (string.IsNullOrEmpty(userIdStr) || !int.TryParse(userIdStr, out int currentUserId))
|
||
return Unauthorized("Invalid or missing user ID");
|
||
|
||
// Which flows is this user part of, and what role in each?
|
||
var flows = await _db.ItApprovalFlows
|
||
.Where(f => f.HodUserId == currentUserId ||
|
||
f.GroupItHodUserId == currentUserId ||
|
||
f.FinHodUserId == currentUserId ||
|
||
f.MgmtUserId == currentUserId)
|
||
.ToListAsync();
|
||
|
||
if (!flows.Any())
|
||
return Ok(new { roles = Array.Empty<string>(), data = Array.Empty<object>() });
|
||
|
||
var flowRoleMap = flows.ToDictionary(
|
||
f => f.ItApprovalFlowId,
|
||
f => f.HodUserId == currentUserId ? "HOD" :
|
||
f.GroupItHodUserId == currentUserId ? "GIT_HOD" :
|
||
f.FinHodUserId == currentUserId ? "FIN_HOD" : "MGMT"
|
||
);
|
||
|
||
var statuses = await _db.ItRequestStatus
|
||
.Include(s => s.Request)
|
||
.Where(s => s.Request != null &&
|
||
s.Request.SubmitDate.Month == month
|
||
&& s.Request.SubmitDate.Year == year
|
||
&& (s.OverallStatus ?? "Draft") != "Draft" // hide drafts
|
||
&& s.OverallStatus != "Cancelled")
|
||
.ToListAsync();
|
||
|
||
var results = statuses
|
||
.Where(s => flowRoleMap.ContainsKey(s.ItApprovalFlowId))
|
||
.Select(s =>
|
||
{
|
||
var role = flowRoleMap[s.ItApprovalFlowId];
|
||
|
||
// quick flags
|
||
bool hodApproved = s.HodStatus == "Approved";
|
||
bool gitApproved = s.GitHodStatus == "Approved";
|
||
bool finApproved = s.FinHodStatus == "Approved";
|
||
bool anyRejected = (s.HodStatus == "Rejected" ||
|
||
s.GitHodStatus == "Rejected" ||
|
||
s.FinHodStatus == "Rejected" ||
|
||
s.MgmtStatus == "Rejected");
|
||
|
||
string currentUserStatus = "N/A";
|
||
bool canApprove = false;
|
||
|
||
if (role == "HOD")
|
||
{
|
||
currentUserStatus = s.HodStatus ?? "Pending";
|
||
canApprove = (currentUserStatus == "Pending"); // first approver always sees its own pending
|
||
}
|
||
else if (role == "GIT_HOD")
|
||
{
|
||
// only after HOD approved and not previously rejected
|
||
if (!anyRejected && hodApproved)
|
||
{
|
||
currentUserStatus = s.GitHodStatus ?? "Pending";
|
||
canApprove = (currentUserStatus == "Pending");
|
||
}
|
||
else
|
||
{
|
||
// don’t surface “pending” for future approver
|
||
currentUserStatus = (s.GitHodStatus ?? "N/A");
|
||
}
|
||
}
|
||
else if (role == "FIN_HOD")
|
||
{
|
||
if (!anyRejected && hodApproved && gitApproved)
|
||
{
|
||
currentUserStatus = s.FinHodStatus ?? "Pending";
|
||
canApprove = (currentUserStatus == "Pending");
|
||
}
|
||
else
|
||
{
|
||
currentUserStatus = (s.FinHodStatus ?? "N/A");
|
||
}
|
||
}
|
||
else // MGMT
|
||
{
|
||
if (!anyRejected && hodApproved && gitApproved && finApproved)
|
||
{
|
||
currentUserStatus = s.MgmtStatus ?? "Pending";
|
||
canApprove = (currentUserStatus == "Pending");
|
||
}
|
||
else
|
||
{
|
||
currentUserStatus = (s.MgmtStatus ?? "N/A");
|
||
}
|
||
}
|
||
|
||
// include row ONLY IF:
|
||
// - this approver can act now (pending for them), OR
|
||
// - this approver already decided (for Completed tab)
|
||
bool includeForUser =
|
||
canApprove ||
|
||
currentUserStatus == "Approved" ||
|
||
currentUserStatus == "Rejected";
|
||
|
||
return new
|
||
{
|
||
s.StatusId,
|
||
s.ItRequestId,
|
||
s.Request.StaffName,
|
||
s.Request.DepartmentName,
|
||
SubmitDate = s.Request.SubmitDate,
|
||
s.HodStatus,
|
||
s.GitHodStatus,
|
||
s.FinHodStatus,
|
||
s.MgmtStatus,
|
||
OverallStatus = s.OverallStatus,
|
||
Role = role,
|
||
CurrentUserStatus = currentUserStatus,
|
||
CanApprove = canApprove,
|
||
IsOverallRejected = anyRejected
|
||
};
|
||
})
|
||
.Where(r => r != null && (r.CanApprove || r.CurrentUserStatus == "Approved" || r.CurrentUserStatus == "Rejected"))
|
||
.ToList();
|
||
|
||
return Ok(new
|
||
{
|
||
roles = flowRoleMap.Values.Distinct().ToList(),
|
||
data = results
|
||
});
|
||
}
|
||
|
||
|
||
|
||
// --------------------------------------------------------------------
|
||
// POST: /ItRequestAPI/approveReject
|
||
// --------------------------------------------------------------------
|
||
public class ApproveRejectDto
|
||
{
|
||
public int StatusId { get; set; }
|
||
public string Decision { get; set; } = string.Empty; // "Approved" or "Rejected"
|
||
public string? Comment { get; set; }
|
||
}
|
||
|
||
[HttpPost("approveReject")]
|
||
public async Task<IActionResult> ApproveReject([FromBody] ApproveRejectDto dto)
|
||
{
|
||
if (dto == null || string.IsNullOrWhiteSpace(dto.Decision))
|
||
return BadRequest(new { message = "Invalid request" });
|
||
|
||
var userIdStr = _userManager.GetUserId(User);
|
||
if (string.IsNullOrEmpty(userIdStr) || !int.TryParse(userIdStr, out int currentUserId))
|
||
return Unauthorized("Invalid user");
|
||
|
||
var status = await _db.ItRequestStatus
|
||
.Include(s => s.Flow)
|
||
.Include(s => s.Request)
|
||
.FirstOrDefaultAsync(s => s.StatusId == dto.StatusId);
|
||
|
||
if (status == null) return NotFound("Status not found");
|
||
|
||
var overall = status.OverallStatus ?? "Draft";
|
||
|
||
// Drafts cannot be approved/rejected ever
|
||
if (overall == "Draft")
|
||
return StatusCode(409, new { message = "This request isn’t submitted yet. Approvals are only allowed once it is Pending." });
|
||
|
||
// CANCELLED: hard stop
|
||
if (string.Equals(overall, "Cancelled", StringComparison.OrdinalIgnoreCase))
|
||
return StatusCode(409, new { message = "This request has been Cancelled and cannot be approved or rejected." });
|
||
|
||
// If any earlier stage already rejected, block
|
||
if (status.HodStatus == "Rejected" || status.GitHodStatus == "Rejected" ||
|
||
status.FinHodStatus == "Rejected" || status.MgmtStatus == "Rejected")
|
||
return BadRequest(new { message = "Request already rejected by a previous approver." });
|
||
|
||
var now = DateTime.Now;
|
||
string decision = dto.Decision.Trim();
|
||
|
||
if (status.Flow.HodUserId == currentUserId && status.HodStatus == "Pending")
|
||
{
|
||
status.HodStatus = decision; status.HodSubmitDate = now;
|
||
}
|
||
else if (status.Flow.GroupItHodUserId == currentUserId && status.GitHodStatus == "Pending" && status.HodStatus == "Approved")
|
||
{
|
||
status.GitHodStatus = decision; status.GitHodSubmitDate = now;
|
||
}
|
||
else if (status.Flow.FinHodUserId == currentUserId && status.FinHodStatus == "Pending" &&
|
||
status.HodStatus == "Approved" && status.GitHodStatus == "Approved")
|
||
{
|
||
status.FinHodStatus = decision; status.FinHodSubmitDate = now;
|
||
}
|
||
else if (status.Flow.MgmtUserId == currentUserId && status.MgmtStatus == "Pending" &&
|
||
status.HodStatus == "Approved" && status.GitHodStatus == "Approved" && status.FinHodStatus == "Approved")
|
||
{
|
||
status.MgmtStatus = decision; status.MgmtSubmitDate = now;
|
||
}
|
||
else
|
||
{
|
||
return BadRequest(new { message = "Not authorized to act at this stage." });
|
||
}
|
||
|
||
if (decision == "Rejected")
|
||
status.OverallStatus = "Rejected";
|
||
else if (status.HodStatus == "Approved" && status.GitHodStatus == "Approved" &&
|
||
status.FinHodStatus == "Approved" && status.MgmtStatus == "Approved")
|
||
status.OverallStatus = "Approved";
|
||
|
||
await _db.SaveChangesAsync();
|
||
return Ok(new { message = "Decision recorded successfully." });
|
||
}
|
||
|
||
#endregion
|
||
|
||
// --------------------------------------------------------------------
|
||
// GET: /ItRequestAPI/myRequests
|
||
// --------------------------------------------------------------------
|
||
[HttpGet("myRequests")]
|
||
public async Task<IActionResult> MyRequests([FromQuery] string? status = null,
|
||
[FromQuery] DateTime? from = null,
|
||
[FromQuery] DateTime? to = null,
|
||
[FromQuery] int page = 1,
|
||
[FromQuery] int pageSize = 50)
|
||
{
|
||
var userIdStr = _userManager.GetUserId(User);
|
||
if (string.IsNullOrEmpty(userIdStr) || !int.TryParse(userIdStr, out int userId))
|
||
return Unauthorized("Invalid user");
|
||
|
||
var q = _db.ItRequests
|
||
.Join(_db.ItRequestStatus, r => r.ItRequestId, s => s.ItRequestId, (r, s) => new { r, s })
|
||
.Where(x => x.r.UserId == userId)
|
||
.AsQueryable();
|
||
|
||
if (!string.IsNullOrWhiteSpace(status))
|
||
q = q.Where(x => x.s.OverallStatus == status);
|
||
|
||
if (from.HasValue) q = q.Where(x => x.r.SubmitDate >= from.Value);
|
||
if (to.HasValue) q = q.Where(x => x.r.SubmitDate < to.Value.AddDays(1));
|
||
|
||
var total = await q.CountAsync();
|
||
var data = await q
|
||
.OrderByDescending(x => x.r.SubmitDate)
|
||
.Skip((page - 1) * pageSize)
|
||
.Take(pageSize)
|
||
.Select(x => new
|
||
{
|
||
x.r.ItRequestId,
|
||
x.s.StatusId,
|
||
x.r.StaffName,
|
||
x.r.DepartmentName,
|
||
x.r.CompanyName,
|
||
x.r.RequiredDate,
|
||
x.r.SubmitDate,
|
||
OverallStatus = x.s.OverallStatus,
|
||
|
||
// extra fields used by UI action buttons
|
||
IsLockedForEdit = x.r.IsLockedForEdit,
|
||
EditableUntil = x.r.EditableUntil,
|
||
HodStatus = x.s.HodStatus,
|
||
GitHodStatus = x.s.GitHodStatus,
|
||
FinHodStatus = x.s.FinHodStatus,
|
||
MgmtStatus = x.s.MgmtStatus
|
||
})
|
||
.ToListAsync();
|
||
|
||
// post-shape with flags (kept in-memory to keep SQL simple)
|
||
var shaped = data.Select(d =>
|
||
{
|
||
var overall = d.OverallStatus ?? "Draft";
|
||
var remaining = d.EditableUntil.HasValue
|
||
? Math.Max(0, (int)(d.EditableUntil.Value - DateTime.UtcNow).TotalSeconds)
|
||
: 0;
|
||
|
||
bool isDraft = overall == "Draft";
|
||
bool canEditDraft = isDraft && !d.IsLockedForEdit && remaining > 0;
|
||
|
||
bool allPending =
|
||
(d.HodStatus ?? "Pending") == "Pending" &&
|
||
(d.GitHodStatus ?? "Pending") == "Pending" &&
|
||
(d.FinHodStatus ?? "Pending") == "Pending" &&
|
||
(d.MgmtStatus ?? "Pending") == "Pending";
|
||
|
||
bool canCancel = isDraft || (ALLOW_PENDING_CANCEL && overall == "Pending" && allPending);
|
||
|
||
|
||
// optional: hint URL for your "View" button in Draft tab
|
||
var editUrlHint = $"/IT/ApprovalDashboard/Edit?statusId={d.StatusId}";
|
||
|
||
return new
|
||
{
|
||
d.ItRequestId,
|
||
d.StatusId,
|
||
d.StaffName,
|
||
d.DepartmentName,
|
||
d.CompanyName,
|
||
d.RequiredDate,
|
||
d.SubmitDate,
|
||
OverallStatus = overall,
|
||
canEditDraft,
|
||
canCancel,
|
||
editUrlHint,
|
||
remainingSeconds = remaining
|
||
};
|
||
}).ToList();
|
||
|
||
return Ok(new { total, page, pageSize, data = shaped });
|
||
|
||
}
|
||
|
||
// --------------------------------------------------------------------
|
||
// Flows CRUD
|
||
// --------------------------------------------------------------------
|
||
[HttpGet("flows")]
|
||
public async Task<IActionResult> GetFlows()
|
||
{
|
||
var flows = await _db.ItApprovalFlows.ToListAsync();
|
||
return Ok(flows);
|
||
}
|
||
|
||
[HttpPost("flows")]
|
||
public async Task<IActionResult> CreateFlow([FromBody] ItApprovalFlow flow)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(flow.FlowName))
|
||
return BadRequest(new { message = "Flow name is required" });
|
||
|
||
_db.ItApprovalFlows.Add(flow);
|
||
await _db.SaveChangesAsync();
|
||
return Ok(new { message = "Flow created" });
|
||
}
|
||
|
||
[HttpPut("flows/{id}")]
|
||
public async Task<IActionResult> UpdateFlow(int id, [FromBody] ItApprovalFlow updated)
|
||
{
|
||
var existing = await _db.ItApprovalFlows.FindAsync(id);
|
||
if (existing == null) return NotFound();
|
||
|
||
existing.FlowName = updated.FlowName;
|
||
existing.HodUserId = updated.HodUserId;
|
||
existing.GroupItHodUserId = updated.GroupItHodUserId;
|
||
existing.FinHodUserId = updated.FinHodUserId;
|
||
existing.MgmtUserId = updated.MgmtUserId;
|
||
|
||
await _db.SaveChangesAsync();
|
||
return Ok(new { message = "Flow updated" });
|
||
}
|
||
|
||
[HttpDelete("flows/{id}")]
|
||
public async Task<IActionResult> DeleteFlow(int id)
|
||
{
|
||
var flow = await _db.ItApprovalFlows.FindAsync(id);
|
||
if (flow == null) return NotFound();
|
||
|
||
bool inUse = await _db.ItRequestStatus.AnyAsync(s => s.ItApprovalFlowId == id);
|
||
if (inUse) return BadRequest(new { message = "Cannot delete flow; it is in use." });
|
||
|
||
_db.ItApprovalFlows.Remove(flow);
|
||
await _db.SaveChangesAsync();
|
||
return Ok(new { message = "Flow deleted" });
|
||
}
|
||
|
||
// --------------------------------------------------------------------
|
||
// DTOs for create
|
||
// --------------------------------------------------------------------
|
||
public class CreateItRequestDto
|
||
{
|
||
// UserId ignored by server (derived from token)
|
||
public string StaffName { get; set; } = string.Empty; // client may send, server will override
|
||
public string? CompanyName { get; set; } // override
|
||
public string? DepartmentName { get; set; } // override
|
||
|
||
public string? Designation { get; set; }
|
||
public string? Location { get; set; }
|
||
public string? EmploymentStatus { get; set; }
|
||
public DateTime? ContractEndDate { get; set; }
|
||
public DateTime RequiredDate { get; set; }
|
||
public string? PhoneExt { get; set; }
|
||
|
||
public List<HardwareDto> Hardware { get; set; } = new();
|
||
public List<EmailDto> Emails { get; set; } = new();
|
||
public List<OsreqDto> OSReqs { get; set; } = new();
|
||
public List<SoftwareDto> Software { get; set; } = new();
|
||
public List<SharedPermDto> SharedPerms { get; set; } = new();
|
||
|
||
public int? EditWindowHours { get; set; }
|
||
public bool? SendNow { get; set; }
|
||
}
|
||
|
||
public class HardwareDto
|
||
{
|
||
public string Category { get; set; } = "";
|
||
public string? Purpose { get; set; }
|
||
public string? Justification { get; set; }
|
||
public string? OtherDescription { get; set; }
|
||
}
|
||
|
||
// NOTE: as requested, Email UI only collects ProposedAddress.
|
||
// Purpose/Notes will be stored as nulls.
|
||
public class EmailDto
|
||
{
|
||
public string? ProposedAddress { get; set; }
|
||
}
|
||
|
||
public class OsreqDto
|
||
{
|
||
public string? RequirementText { get; set; }
|
||
}
|
||
|
||
public class SoftwareDto
|
||
{
|
||
public string Bucket { get; set; } = ""; // General | Utility | Custom
|
||
public string Name { get; set; } = "";
|
||
public string? OtherName { get; set; }
|
||
public string? Notes { get; set; }
|
||
}
|
||
|
||
public class SharedPermDto
|
||
{
|
||
public string? ShareName { get; set; }
|
||
public bool CanRead { get; set; }
|
||
public bool CanWrite { get; set; }
|
||
public bool CanDelete { get; set; }
|
||
public bool CanRemove { get; set; }
|
||
}
|
||
|
||
|
||
// --------------------------------------------------------------------
|
||
// POST: /ItRequestAPI/create
|
||
// Creates a new IT Request and starts an edit window (default 24 hours)
|
||
// --------------------------------------------------------------------
|
||
[HttpPost("create")]
|
||
public async Task<IActionResult> CreateRequest([FromBody] CreateItRequestDto dto)
|
||
{
|
||
if (dto == null)
|
||
return BadRequest(new { message = "Invalid request payload" });
|
||
|
||
if (dto.RequiredDate == default)
|
||
return BadRequest(new { message = "RequiredDate is required." });
|
||
|
||
// --- Server-side guard: RequiredDate >= today + 7 (local server date) ---
|
||
var minDate = DateTime.Today.AddDays(7);
|
||
if (dto.RequiredDate.Date < minDate)
|
||
return BadRequest(new { message = $"RequiredDate must be at least {minDate:yyyy-MM-dd} or later." });
|
||
|
||
// --- Server-side guard: SharedPerms cap 6 (trim or reject; here we trim silently) ---
|
||
if (dto.SharedPerms != null && dto.SharedPerms.Count > 6)
|
||
dto.SharedPerms = dto.SharedPerms.Take(6).ToList();
|
||
|
||
// --- Optional normalization: avoid conflicting hardware choices ---
|
||
// If DesktopAllIn is present, drop NotebookAllIn/NotebookOnly
|
||
bool hasDesktopAllIn = dto.Hardware?.Any(h => string.Equals(h.Category, "DesktopAllIn", StringComparison.OrdinalIgnoreCase)) == true;
|
||
if (hasDesktopAllIn && dto.Hardware != null)
|
||
dto.Hardware = dto.Hardware.Where(h => !string.Equals(h.Category, "NotebookAllIn", StringComparison.OrdinalIgnoreCase)
|
||
&& !string.Equals(h.Category, "NotebookOnly", StringComparison.OrdinalIgnoreCase))
|
||
.ToList();
|
||
|
||
// If NotebookAllIn is present, drop DesktopAllIn/DesktopOnly
|
||
bool hasNotebookAllIn = dto.Hardware?.Any(h => string.Equals(h.Category, "NotebookAllIn", StringComparison.OrdinalIgnoreCase)) == true;
|
||
if (hasNotebookAllIn && dto.Hardware != null)
|
||
dto.Hardware = dto.Hardware.Where(h => !string.Equals(h.Category, "DesktopAllIn", StringComparison.OrdinalIgnoreCase)
|
||
&& !string.Equals(h.Category, "DesktopOnly", StringComparison.OrdinalIgnoreCase))
|
||
.ToList();
|
||
|
||
try
|
||
{
|
||
// --- Validate and get current user -----------------------------------------------------
|
||
var userIdStr = _userManager.GetUserId(User);
|
||
if (string.IsNullOrEmpty(userIdStr) || !int.TryParse(userIdStr, out int currentUserId))
|
||
return Unauthorized("Invalid user");
|
||
|
||
// --- Lookup user snapshot --------------------------------------------------------------
|
||
var user = await _db.Users.FirstOrDefaultAsync(u => u.Id == currentUserId);
|
||
if (user == null) return NotFound("User not found.");
|
||
|
||
var dept = await _db.Departments.FirstOrDefaultAsync(d => d.DepartmentId == user.departmentId);
|
||
var comp = dept != null
|
||
? await _db.Companies.FirstOrDefaultAsync(c => c.CompanyId == dept.CompanyId)
|
||
: null;
|
||
|
||
var snapStaffName = user.FullName ?? user.UserName ?? "(unknown)";
|
||
var snapDepartmentName = dept?.DepartmentName ?? "";
|
||
var snapCompanyName = comp?.CompanyName ?? "";
|
||
|
||
// --- Determine edit window hours -------------------------------------------------------
|
||
int windowHours = 24;
|
||
if (dto.EditWindowHours.HasValue)
|
||
windowHours = Math.Max(1, Math.Min(dto.EditWindowHours.Value, 24 * 14));
|
||
else if (int.TryParse(Request.Headers["X-Edit-Window-Hours"], out var h))
|
||
windowHours = Math.Max(1, Math.Min(h, 24 * 14));
|
||
else if (int.TryParse(Request.Query["editWindowHours"], out var q))
|
||
windowHours = Math.Max(1, Math.Min(q, 24 * 14));
|
||
|
||
var nowUtc = DateTime.UtcNow;
|
||
|
||
// --- Insert main request ---------------------------------------------------------------
|
||
var request = new ItRequest
|
||
{
|
||
UserId = currentUserId,
|
||
|
||
StaffName = snapStaffName,
|
||
DepartmentName = snapDepartmentName,
|
||
CompanyName = snapCompanyName,
|
||
|
||
Designation = dto.Designation,
|
||
Location = dto.Location,
|
||
EmploymentStatus = dto.EmploymentStatus,
|
||
ContractEndDate = dto.ContractEndDate,
|
||
|
||
RequiredDate = dto.RequiredDate,
|
||
PhoneExt = dto.PhoneExt,
|
||
SubmitDate = DateTime.Now,
|
||
|
||
FirstSubmittedAt = nowUtc,
|
||
EditableUntil = nowUtc.AddHours(windowHours),
|
||
IsLockedForEdit = false
|
||
};
|
||
|
||
_db.ItRequests.Add(request);
|
||
await _db.SaveChangesAsync();
|
||
|
||
// --- Children --------------------------------------------------------------------------
|
||
if (dto.Hardware != null)
|
||
foreach (var hw in dto.Hardware)
|
||
_db.ItRequestHardwares.Add(new ItRequestHardware
|
||
{
|
||
ItRequestId = request.ItRequestId,
|
||
Category = hw.Category,
|
||
Purpose = hw.Purpose,
|
||
Justification = hw.Justification,
|
||
OtherDescription = hw.OtherDescription
|
||
});
|
||
|
||
if (dto.Emails != null)
|
||
foreach (var em in dto.Emails)
|
||
_db.ItRequestEmails.Add(new ItRequestEmail
|
||
{
|
||
ItRequestId = request.ItRequestId,
|
||
Purpose = null,
|
||
ProposedAddress = em.ProposedAddress,
|
||
Notes = null
|
||
});
|
||
|
||
if (dto.OSReqs != null)
|
||
foreach (var os in dto.OSReqs)
|
||
_db.ItRequestOsRequirement.Add(new ItRequestOsRequirement
|
||
{
|
||
ItRequestId = request.ItRequestId,
|
||
RequirementText = os.RequirementText
|
||
});
|
||
|
||
if (dto.Software != null)
|
||
foreach (var sw in dto.Software)
|
||
_db.ItRequestSoftware.Add(new ItRequestSoftware
|
||
{
|
||
ItRequestId = request.ItRequestId,
|
||
Bucket = sw.Bucket,
|
||
Name = sw.Name,
|
||
OtherName = sw.OtherName,
|
||
Notes = sw.Notes
|
||
});
|
||
|
||
if (dto.SharedPerms != null)
|
||
foreach (var sp in dto.SharedPerms)
|
||
_db.ItRequestSharedPermission.Add(new ItRequestSharedPermission
|
||
{
|
||
ItRequestId = request.ItRequestId,
|
||
ShareName = sp.ShareName,
|
||
CanRead = sp.CanRead,
|
||
CanWrite = sp.CanWrite,
|
||
CanDelete = sp.CanDelete,
|
||
CanRemove = sp.CanRemove
|
||
});
|
||
|
||
await _db.SaveChangesAsync();
|
||
|
||
// --- Default approval flow -------------------------------------------------------------
|
||
var flow = await _db.ItApprovalFlows.FirstOrDefaultAsync();
|
||
if (flow == null)
|
||
return BadRequest(new { message = "No IT Approval Flow configured" });
|
||
|
||
var status = new ItRequestStatus
|
||
{
|
||
ItRequestId = request.ItRequestId,
|
||
ItApprovalFlowId = flow.ItApprovalFlowId,
|
||
HodStatus = "Pending",
|
||
GitHodStatus = "Pending",
|
||
FinHodStatus = "Pending",
|
||
MgmtStatus = "Pending",
|
||
OverallStatus = "Draft"
|
||
};
|
||
|
||
_db.ItRequestStatus.Add(status);
|
||
await _db.SaveChangesAsync();
|
||
|
||
if (dto.SendNow == true)
|
||
{
|
||
request.IsLockedForEdit = true;
|
||
status.OverallStatus = "Pending";
|
||
await _db.SaveChangesAsync();
|
||
}
|
||
|
||
return Ok(new
|
||
{
|
||
message = "IT request created successfully. You can edit it for a limited time.",
|
||
requestId = request.ItRequestId,
|
||
statusId = status.StatusId,
|
||
editableUntil = request.EditableUntil,
|
||
editWindowHours = windowHours,
|
||
overallStatus = status.OverallStatus
|
||
});
|
||
}
|
||
catch (UnauthorizedAccessException ex)
|
||
{
|
||
return Unauthorized(ex.Message);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return StatusCode(500, new { message = "Error creating IT request", detail = ex.Message });
|
||
}
|
||
}
|
||
|
||
|
||
|
||
private static bool ShouldLock(ItRequest r) =>
|
||
!r.IsLockedForEdit && r.EditableUntil.HasValue && DateTime.UtcNow > r.EditableUntil.Value;
|
||
|
||
private async Task LockOnlyAsync(ItRequest r)
|
||
{
|
||
if (r.IsLockedForEdit) return;
|
||
r.IsLockedForEdit = true;
|
||
await _db.SaveChangesAsync();
|
||
}
|
||
|
||
|
||
// read current edit state (also lazy-lock if expired)
|
||
[HttpGet("editWindow/{statusId:int}")]
|
||
public async Task<IActionResult> GetEditWindow(int statusId)
|
||
{
|
||
var s = await _db.ItRequestStatus.Include(x => x.Request).FirstOrDefaultAsync(x => x.StatusId == statusId);
|
||
if (s == null) return NotFound("Status not found");
|
||
var r = s.Request;
|
||
|
||
if (ShouldLock(r)) await LockOnlyAsync(r);
|
||
|
||
var remaining = r.EditableUntil.HasValue ? Math.Max(0, (int)(r.EditableUntil.Value - DateTime.UtcNow).TotalSeconds) : 0;
|
||
return Ok(new
|
||
{
|
||
overallStatus = s.OverallStatus ?? "Draft",
|
||
isEditable = !r.IsLockedForEdit && (s.OverallStatus ?? "Draft") == "Draft" && remaining > 0,
|
||
remainingSeconds = remaining,
|
||
editableUntil = r.EditableUntil
|
||
});
|
||
}
|
||
|
||
// PUT: full draft update (same shape as create)
|
||
public class UpdateDraftDto : CreateItRequestDto { public int StatusId { get; set; } }
|
||
|
||
[HttpPut("edit/{statusId:int}")]
|
||
public async Task<IActionResult> UpdateDraft(int statusId, [FromBody] UpdateDraftDto dto)
|
||
{
|
||
int currentUserId; try { currentUserId = GetCurrentUserIdOrThrow(); } catch (Exception ex) { return Unauthorized(ex.Message); }
|
||
|
||
var s = await _db.ItRequestStatus.Include(x => x.Request).FirstOrDefaultAsync(x => x.StatusId == statusId);
|
||
if (s == null) return NotFound("Status not found");
|
||
var r = s.Request;
|
||
|
||
if (r.UserId != currentUserId) return Unauthorized("You can only edit your own request");
|
||
if (ShouldLock(r)) await LockOnlyAsync(r);
|
||
if (r.IsLockedForEdit || (s.OverallStatus ?? "Draft") != "Draft") return StatusCode(423, "Edit window closed.");
|
||
|
||
// --- Server-side guard: RequiredDate >= today + 7 ---
|
||
var minDate = DateTime.Today.AddDays(7);
|
||
if (dto.RequiredDate.Date < minDate)
|
||
return BadRequest(new { message = $"RequiredDate must be at least {minDate:yyyy-MM-dd} or later." });
|
||
|
||
// --- Cap shared perms to 6 ---
|
||
if (dto.SharedPerms != null && dto.SharedPerms.Count > 6)
|
||
dto.SharedPerms = dto.SharedPerms.Take(6).ToList();
|
||
|
||
// --- Optional normalization of conflicting hardware ---
|
||
bool hasDesktopAllIn = dto.Hardware?.Any(h => string.Equals(h.Category, "DesktopAllIn", StringComparison.OrdinalIgnoreCase)) == true;
|
||
if (hasDesktopAllIn && dto.Hardware != null)
|
||
dto.Hardware = dto.Hardware.Where(h => !string.Equals(h.Category, "NotebookAllIn", StringComparison.OrdinalIgnoreCase)
|
||
&& !string.Equals(h.Category, "NotebookOnly", StringComparison.OrdinalIgnoreCase)).ToList();
|
||
bool hasNotebookAllIn = dto.Hardware?.Any(h => string.Equals(h.Category, "NotebookAllIn", StringComparison.OrdinalIgnoreCase)) == true;
|
||
if (hasNotebookAllIn && dto.Hardware != null)
|
||
dto.Hardware = dto.Hardware.Where(h => !string.Equals(h.Category, "DesktopAllIn", StringComparison.OrdinalIgnoreCase)
|
||
&& !string.Equals(h.Category, "DesktopOnly", StringComparison.OrdinalIgnoreCase)).ToList();
|
||
|
||
// update simple fields
|
||
r.Designation = dto.Designation;
|
||
r.Location = dto.Location;
|
||
r.EmploymentStatus = dto.EmploymentStatus;
|
||
r.ContractEndDate = dto.ContractEndDate;
|
||
r.RequiredDate = dto.RequiredDate;
|
||
r.PhoneExt = dto.PhoneExt;
|
||
|
||
// replace children
|
||
var id = r.ItRequestId;
|
||
|
||
_db.ItRequestHardwares.RemoveRange(_db.ItRequestHardwares.Where(x => x.ItRequestId == id));
|
||
if (dto.Hardware != null)
|
||
foreach (var x in dto.Hardware)
|
||
_db.ItRequestHardwares.Add(new ItRequestHardware { ItRequestId = id, Category = x.Category, Purpose = x.Purpose, Justification = x.Justification, OtherDescription = x.OtherDescription });
|
||
|
||
_db.ItRequestEmails.RemoveRange(_db.ItRequestEmails.Where(x => x.ItRequestId == id));
|
||
if (dto.Emails != null)
|
||
foreach (var x in dto.Emails)
|
||
_db.ItRequestEmails.Add(new ItRequestEmail { ItRequestId = id, Purpose = null, ProposedAddress = x.ProposedAddress, Notes = null });
|
||
|
||
_db.ItRequestOsRequirement.RemoveRange(_db.ItRequestOsRequirement.Where(x => x.ItRequestId == id));
|
||
if (dto.OSReqs != null)
|
||
foreach (var x in dto.OSReqs)
|
||
_db.ItRequestOsRequirement.Add(new ItRequestOsRequirement { ItRequestId = id, RequirementText = x.RequirementText });
|
||
|
||
_db.ItRequestSoftware.RemoveRange(_db.ItRequestSoftware.Where(x => x.ItRequestId == id));
|
||
if (dto.Software != null)
|
||
foreach (var x in dto.Software)
|
||
_db.ItRequestSoftware.Add(new ItRequestSoftware { ItRequestId = id, Bucket = x.Bucket, Name = x.Name, OtherName = x.OtherName, Notes = x.Notes });
|
||
|
||
_db.ItRequestSharedPermission.RemoveRange(_db.ItRequestSharedPermission.Where(x => x.ItRequestId == id));
|
||
if (dto.SharedPerms != null)
|
||
foreach (var x in dto.SharedPerms)
|
||
_db.ItRequestSharedPermission.Add(new ItRequestSharedPermission { ItRequestId = id, ShareName = x.ShareName, CanRead = x.CanRead, CanWrite = x.CanWrite, CanDelete = x.CanDelete, CanRemove = x.CanRemove });
|
||
|
||
await _db.SaveChangesAsync();
|
||
|
||
var remaining = r.EditableUntil.HasValue ? Math.Max(0, (int)(r.EditableUntil.Value - DateTime.UtcNow).TotalSeconds) : 0;
|
||
return Ok(new { message = "Draft saved", remainingSeconds = remaining });
|
||
}
|
||
|
||
|
||
// POST: send early
|
||
[HttpPost("sendNow/{statusId:int}")]
|
||
public async Task<IActionResult> SendNow(int statusId)
|
||
{
|
||
int uid;
|
||
try { uid = GetCurrentUserIdOrThrow(); }
|
||
catch (Exception ex) { return Unauthorized(ex.Message); }
|
||
|
||
var s = await _db.ItRequestStatus
|
||
.Include(x => x.Request)
|
||
.FirstOrDefaultAsync(x => x.StatusId == statusId);
|
||
|
||
if (s == null) return NotFound("Status not found");
|
||
if (s.Request.UserId != uid) return Unauthorized("Only owner can send");
|
||
|
||
// Allow SendNow as long as it’s still a Draft (even if the edit window already locked it).
|
||
if ((s.OverallStatus ?? "Draft") != "Draft")
|
||
return BadRequest(new { message = "Already sent or not in Draft." });
|
||
|
||
// Ensure locked, then make it visible to approvers.
|
||
if (!s.Request.IsLockedForEdit) s.Request.IsLockedForEdit = true;
|
||
s.OverallStatus = "Pending";
|
||
|
||
await _db.SaveChangesAsync();
|
||
|
||
return Ok(new { message = "Sent to approval", overallStatus = s.OverallStatus });
|
||
}
|
||
|
||
|
||
|
||
|
||
// --------------------------------------------------------------------
|
||
// GET: /ItRequestAPI/request/{statusId}
|
||
// Adds lazy-lock and returns full request details for Edit page
|
||
// --------------------------------------------------------------------
|
||
[HttpGet("request/{statusId}")]
|
||
public async Task<IActionResult> GetRequestDetail(int statusId)
|
||
{
|
||
// auth
|
||
var userIdStr = _userManager.GetUserId(User);
|
||
if (string.IsNullOrEmpty(userIdStr) || !int.TryParse(userIdStr, out int currentUserId))
|
||
return Unauthorized("Invalid user");
|
||
|
||
// load status + children
|
||
var status = await _db.ItRequestStatus
|
||
.Include(s => s.Request).ThenInclude(r => r!.Hardware)
|
||
.Include(s => s.Request).ThenInclude(r => r!.Emails)
|
||
.Include(s => s.Request).ThenInclude(r => r!.OsRequirements)
|
||
.Include(s => s.Request).ThenInclude(r => r!.Software)
|
||
.Include(s => s.Request).ThenInclude(r => r!.SharedPermissions)
|
||
.Include(s => s.Flow)
|
||
.FirstOrDefaultAsync(s => s.StatusId == statusId);
|
||
|
||
if (status == null) return NotFound("Request not found");
|
||
|
||
// lazy lock if the edit window has expired
|
||
async Task LazyLockIfExpired(ItRequestStatus s)
|
||
{
|
||
var r = s.Request;
|
||
var expired = !r.IsLockedForEdit && r.EditableUntil.HasValue && DateTime.UtcNow > r.EditableUntil.Value;
|
||
if (!expired) return;
|
||
|
||
r.IsLockedForEdit = true;
|
||
await _db.SaveChangesAsync();
|
||
}
|
||
await LazyLockIfExpired(status);
|
||
|
||
// remaining seconds for countdown
|
||
var remaining = status.Request.EditableUntil.HasValue
|
||
? Math.Max(0, (int)(status.Request.EditableUntil.Value - DateTime.UtcNow).TotalSeconds)
|
||
: 0;
|
||
|
||
// role + canApprove (unchanged)
|
||
string role =
|
||
status.Flow.HodUserId == currentUserId ? "HOD" :
|
||
status.Flow.GroupItHodUserId == currentUserId ? "GIT_HOD" :
|
||
status.Flow.FinHodUserId == currentUserId ? "FIN_HOD" :
|
||
status.Flow.MgmtUserId == currentUserId ? "MGMT" :
|
||
"VIEWER";
|
||
|
||
string currentUserStatus = "N/A";
|
||
bool canApprove = false;
|
||
if (string.Equals(status.OverallStatus, "Cancelled", StringComparison.OrdinalIgnoreCase) ||
|
||
string.Equals(status.OverallStatus ?? "Draft", "Draft", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
canApprove = false;
|
||
}
|
||
|
||
if (role == "HOD")
|
||
{
|
||
currentUserStatus = status.HodStatus ?? "Pending";
|
||
canApprove = currentUserStatus == "Pending";
|
||
}
|
||
else if (role == "GIT_HOD")
|
||
{
|
||
currentUserStatus = status.GitHodStatus ?? "Pending";
|
||
canApprove = currentUserStatus == "Pending" && status.HodStatus == "Approved";
|
||
}
|
||
else if (role == "FIN_HOD")
|
||
{
|
||
currentUserStatus = status.FinHodStatus ?? "Pending";
|
||
canApprove = currentUserStatus == "Pending" &&
|
||
status.HodStatus == "Approved" &&
|
||
status.GitHodStatus == "Approved";
|
||
}
|
||
else if (role == "MGMT")
|
||
{
|
||
currentUserStatus = status.MgmtStatus ?? "Pending";
|
||
canApprove = currentUserStatus == "Pending" &&
|
||
status.HodStatus == "Approved" &&
|
||
status.GitHodStatus == "Approved" &&
|
||
status.FinHodStatus == "Approved";
|
||
}
|
||
// No actions allowed on cancelled requests
|
||
|
||
|
||
|
||
// RESPONSE: include full main fields for Edit page
|
||
return Ok(new
|
||
{
|
||
// full request fields you need to prefill
|
||
request = new
|
||
{
|
||
status.Request.ItRequestId,
|
||
status.Request.StaffName,
|
||
status.Request.DepartmentName,
|
||
status.Request.CompanyName,
|
||
status.Request.Designation,
|
||
status.Request.Location,
|
||
status.Request.EmploymentStatus,
|
||
status.Request.ContractEndDate,
|
||
status.Request.RequiredDate,
|
||
status.Request.PhoneExt,
|
||
status.Request.SubmitDate
|
||
},
|
||
|
||
approverRole = role,
|
||
|
||
hardware = status.Request.Hardware?.Select(h => new {
|
||
h.Id,
|
||
Category = h.Category ?? "",
|
||
Purpose = h.Purpose ?? "",
|
||
Justification = h.Justification ?? "",
|
||
OtherDescription = h.OtherDescription ?? ""
|
||
}),
|
||
|
||
emails = status.Request.Emails?.Select(e => new {
|
||
e.Id,
|
||
Purpose = e.Purpose ?? "",
|
||
ProposedAddress = e.ProposedAddress ?? "",
|
||
Notes = e.Notes ?? ""
|
||
}),
|
||
|
||
osreqs = status.Request.OsRequirements?.Select(o => new {
|
||
o.Id,
|
||
RequirementText = o.RequirementText ?? ""
|
||
}),
|
||
|
||
software = status.Request.Software?.Select(sw => new {
|
||
sw.Id,
|
||
Bucket = sw.Bucket ?? "",
|
||
Name = sw.Name ?? "",
|
||
OtherName = sw.OtherName ?? "",
|
||
Notes = sw.Notes ?? ""
|
||
}),
|
||
|
||
sharedPerms = status.Request.SharedPermissions?.Select(sp => new {
|
||
sp.Id,
|
||
ShareName = sp.ShareName ?? "",
|
||
CanRead = sp.CanRead,
|
||
CanWrite = sp.CanWrite,
|
||
CanDelete = sp.CanDelete,
|
||
CanRemove = sp.CanRemove
|
||
}),
|
||
|
||
status = new
|
||
{
|
||
hodStatus = status.HodStatus ?? "Pending",
|
||
gitHodStatus = status.GitHodStatus ?? "Pending",
|
||
finHodStatus = status.FinHodStatus ?? "Pending",
|
||
mgmtStatus = status.MgmtStatus ?? "Pending",
|
||
|
||
hodSubmitDate = status.HodSubmitDate,
|
||
gitHodSubmitDate = status.GitHodSubmitDate,
|
||
finHodSubmitDate = status.FinHodSubmitDate,
|
||
mgmtSubmitDate = status.MgmtSubmitDate,
|
||
|
||
overallStatus = status.OverallStatus ?? "Pending",
|
||
canApprove = canApprove,
|
||
currentUserStatus = currentUserStatus
|
||
},
|
||
|
||
// edit window info for the Edit page
|
||
edit = new
|
||
{
|
||
overallStatus = status.OverallStatus ?? "Draft",
|
||
isEditable = !status.Request.IsLockedForEdit && (status.OverallStatus ?? "Draft") == "Draft" && remaining > 0,
|
||
remainingSeconds = remaining,
|
||
editableUntil = status.Request.EditableUntil
|
||
}
|
||
});
|
||
}
|
||
|
||
|
||
|
||
// --------------------------------------------------------------------
|
||
// GET: /ItRequestAPI/lookups
|
||
// --------------------------------------------------------------------
|
||
[HttpGet("lookups")]
|
||
public IActionResult Lookups()
|
||
{
|
||
var hardwareCategories = new[] {
|
||
"DesktopAllIn","NotebookAllIn","DesktopOnly","NotebookOnly","NotebookBattery","PowerAdapter","Mouse","ExternalHDD","Other"
|
||
};
|
||
var hardwarePurposes = new[] { "NewRecruitment", "Replacement", "Additional" };
|
||
var employmentStatuses = new[] { "Permanent", "Contract", "Temp", "New Staff" };
|
||
var softwareBuckets = new[] { "General", "Utility", "Custom" };
|
||
|
||
return Ok(new
|
||
{
|
||
hardwareCategories,
|
||
hardwarePurposes,
|
||
employmentStatuses,
|
||
softwareBuckets
|
||
});
|
||
}
|
||
|
||
|
||
// --------------------------------------------------------------------
|
||
// POST: /ItRequestAPI/cancel
|
||
public class CancelDto { public int RequestId { get; set; } public string? Reason { get; set; } }
|
||
|
||
[HttpPost("cancel")]
|
||
public async Task<IActionResult> CancelRequest([FromBody] CancelDto dto)
|
||
{
|
||
int userId;
|
||
try { userId = GetCurrentUserIdOrThrow(); }
|
||
catch (UnauthorizedAccessException ex) { return Unauthorized(ex.Message); }
|
||
|
||
var req = await _db.ItRequests.FirstOrDefaultAsync(r => r.ItRequestId == dto.RequestId);
|
||
if (req == null) return NotFound("Request not found");
|
||
if (req.UserId != userId) return Unauthorized("You can only cancel your own requests");
|
||
|
||
var s = await _db.ItRequestStatus.FirstOrDefaultAsync(x => x.ItRequestId == req.ItRequestId);
|
||
if (s == null) return NotFound("Status row not found");
|
||
|
||
var overall = s.OverallStatus ?? "Draft";
|
||
|
||
// Allow cancel:
|
||
// - Draft (always)
|
||
// - Pending (only if ALLOW_PENDING_CANCEL == true) and no approvals have started
|
||
bool allPending =
|
||
(s.HodStatus ?? "Pending") == "Pending" &&
|
||
(s.GitHodStatus ?? "Pending") == "Pending" &&
|
||
(s.FinHodStatus ?? "Pending") == "Pending" &&
|
||
(s.MgmtStatus ?? "Pending") == "Pending";
|
||
|
||
bool canCancelNow =
|
||
overall == "Draft" ||
|
||
(ALLOW_PENDING_CANCEL && overall == "Pending" && allPending);
|
||
|
||
if (!canCancelNow)
|
||
return BadRequest(new { message = "Cannot cancel after approvals have started or once decided." });
|
||
|
||
s.OverallStatus = "Cancelled";
|
||
req.IsLockedForEdit = true;
|
||
// TODO: persist dto.Reason if you add a column
|
||
|
||
await _db.SaveChangesAsync();
|
||
return Ok(new { message = "Request cancelled", requestId = req.ItRequestId, statusId = s.StatusId, overallStatus = s.OverallStatus });
|
||
}
|
||
|
||
|
||
}
|
||
}
|