From 425ac307bba0dcd9f669dc3b131f7c58dc37ffe3 Mon Sep 17 00:00:00 2001 From: 18631081161 <2088094923@qq.com> Date: Sat, 17 Jan 2026 02:35:54 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=B8=8A=E6=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/AllocationsController.cs | 1 + .../ConsumptionChangeRequestsController.cs | 334 +++++++ .../Controllers/PersonnelController.cs | 32 +- .../Data/ApplicationDbContext.cs | 37 + ...onsumptionReportChangeRequests.Designer.cs | 874 ++++++++++++++++++ ...0324_AddConsumptionReportChangeRequests.cs | 162 ++++ .../ApplicationDbContextModelSnapshot.cs | 126 +++ .../Models/Entities/ConsumptionReport.cs | 18 + .../ConsumptionReportChangeRequest.cs | 91 ++ .../Models/Enums/ChangeRequestStatus.cs | 14 + .../Models/Enums/ChangeRequestType.cs | 12 + src/MilitaryTrainingManagement/Program.cs | 56 ++ .../Implementations/AllocationService.cs | 1 + .../Implementations/PersonnelService.cs | 13 +- .../Services/Interfaces/IPersonnelService.cs | 4 +- src/frontend/src/api/allocations.ts | 66 ++ src/frontend/src/api/index.ts | 2 +- src/frontend/src/api/personnel.ts | 22 + src/frontend/src/layouts/MainLayout.vue | 1 + src/frontend/src/router/index.ts | 6 + src/frontend/src/stores/auth.ts | 1 + src/frontend/src/views/AuditLogs.vue | 11 - .../src/views/allocations/AllocationList.vue | 2 + .../views/allocations/AllocationReport.vue | 177 +++- .../views/allocations/ChangeRequestList.vue | 369 ++++++++ .../src/views/personnel/PersonnelDetail.vue | 143 ++- .../src/views/personnel/PersonnelList.vue | 22 +- 27 files changed, 2546 insertions(+), 51 deletions(-) create mode 100644 src/MilitaryTrainingManagement/Controllers/ConsumptionChangeRequestsController.cs create mode 100644 src/MilitaryTrainingManagement/Migrations/20260116180324_AddConsumptionReportChangeRequests.Designer.cs create mode 100644 src/MilitaryTrainingManagement/Migrations/20260116180324_AddConsumptionReportChangeRequests.cs create mode 100644 src/MilitaryTrainingManagement/Models/Entities/ConsumptionReportChangeRequest.cs create mode 100644 src/MilitaryTrainingManagement/Models/Enums/ChangeRequestStatus.cs create mode 100644 src/MilitaryTrainingManagement/Models/Enums/ChangeRequestType.cs create mode 100644 src/frontend/src/views/allocations/ChangeRequestList.vue diff --git a/src/MilitaryTrainingManagement/Controllers/AllocationsController.cs b/src/MilitaryTrainingManagement/Controllers/AllocationsController.cs index 2adeb1e..637743a 100644 --- a/src/MilitaryTrainingManagement/Controllers/AllocationsController.cs +++ b/src/MilitaryTrainingManagement/Controllers/AllocationsController.cs @@ -428,6 +428,7 @@ public class AllocationsController : BaseApiController cumulativeAmount = r.CumulativeAmount, remarks = r.Remarks, reportedByUserName = r.ReportedByUser?.DisplayName, + reportedByUnitId = r.ReportedByUnitId, reportedAt = r.ReportedAt }); diff --git a/src/MilitaryTrainingManagement/Controllers/ConsumptionChangeRequestsController.cs b/src/MilitaryTrainingManagement/Controllers/ConsumptionChangeRequestsController.cs new file mode 100644 index 0000000..616e95a --- /dev/null +++ b/src/MilitaryTrainingManagement/Controllers/ConsumptionChangeRequestsController.cs @@ -0,0 +1,334 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using MilitaryTrainingManagement.Data; +using MilitaryTrainingManagement.Models.Entities; +using MilitaryTrainingManagement.Models.Enums; + +namespace MilitaryTrainingManagement.Controllers; + +/// +/// 消耗记录删改申请控制器 +/// +[Authorize] +public class ConsumptionChangeRequestsController : BaseApiController +{ + private readonly ApplicationDbContext _context; + private readonly ILogger _logger; + + public ConsumptionChangeRequestsController( + ApplicationDbContext context, + ILogger logger) + { + _context = context; + _logger = logger; + } + + /// + /// 创建删改申请 + /// + [HttpPost] + public async Task Create([FromBody] CreateChangeRequestDto request) + { + var unitId = GetCurrentUnitId(); + var userId = GetCurrentUserId(); + if (unitId == null || userId == null) + return Unauthorized(); + + // 验证消耗记录存在且属于当前单位 + var report = await _context.ConsumptionReports + .Include(r => r.ReportedByUnit) + .FirstOrDefaultAsync(r => r.Id == request.ConsumptionReportId); + + if (report == null) + return NotFound(new { message = "消耗记录不存在" }); + + if (report.ReportedByUnitId != unitId.Value) + return BadRequest(new { message = "只能申请删改本单位的上报记录" }); + + // 检查是否已有待处理的申请 + var existingRequest = await _context.ConsumptionReportChangeRequests + .AnyAsync(r => r.ConsumptionReportId == request.ConsumptionReportId + && r.Status == ChangeRequestStatus.Pending); + + if (existingRequest) + return BadRequest(new { message = "该记录已有待处理的删改申请" }); + + var changeRequest = new ConsumptionReportChangeRequest + { + ConsumptionReportId = request.ConsumptionReportId, + RequestType = request.RequestType, + Reason = request.Reason, + Status = ChangeRequestStatus.Pending, + RequestedByUnitId = unitId.Value, + RequestedByUserId = userId.Value, + RequestedAt = DateTime.UtcNow + }; + + _context.ConsumptionReportChangeRequests.Add(changeRequest); + await _context.SaveChangesAsync(); + + _logger.LogInformation("单位 {UnitId} 创建了消耗记录 {ReportId} 的{Type}申请", + unitId, request.ConsumptionReportId, request.RequestType); + + return Ok(new { message = "申请已提交", id = changeRequest.Id }); + } + + /// + /// 获取待处理的申请列表(上级单位查看下级的申请) + /// + [HttpGet("pending")] + public async Task GetPendingRequests() + { + var unitId = GetCurrentUnitId(); + if (unitId == null) + return Unauthorized(); + + // 获取下级单位的ID列表 + var subordinateIds = await GetSubordinateUnitIds(unitId.Value); + + var requests = await _context.ConsumptionReportChangeRequests + .Include(r => r.ConsumptionReport) + .ThenInclude(cr => cr.AllocationDistribution) + .ThenInclude(d => d.Allocation) + .Include(r => r.RequestedByUnit) + .Include(r => r.RequestedByUser) + .Where(r => r.Status == ChangeRequestStatus.Pending + && subordinateIds.Contains(r.RequestedByUnitId)) + .OrderByDescending(r => r.RequestedAt) + .Select(r => new + { + r.Id, + r.ConsumptionReportId, + RequestType = r.RequestType.ToString(), + r.Reason, + Status = r.Status.ToString(), + r.RequestedAt, + RequestedByUnitName = r.RequestedByUnit.Name, + RequestedByUserName = r.RequestedByUser.DisplayName, + ConsumptionReport = new + { + r.ConsumptionReport.Id, + r.ConsumptionReport.ReportedAmount, + r.ConsumptionReport.ReportedAt, + r.ConsumptionReport.Remarks, + MaterialName = r.ConsumptionReport.AllocationDistribution.Allocation.MaterialName, + Unit = r.ConsumptionReport.AllocationDistribution.Allocation.Unit + } + }) + .ToListAsync(); + + return Ok(requests); + } + + /// + /// 获取本单位的申请列表 + /// + [HttpGet("my")] + public async Task GetMyRequests() + { + var unitId = GetCurrentUnitId(); + if (unitId == null) + return Unauthorized(); + + var requests = await _context.ConsumptionReportChangeRequests + .Include(r => r.ConsumptionReport) + .Include(r => r.ProcessedByUnit) + .Where(r => r.RequestedByUnitId == unitId.Value) + .OrderByDescending(r => r.RequestedAt) + .Select(r => new + { + r.Id, + r.ConsumptionReportId, + RequestType = r.RequestType.ToString(), + r.Reason, + Status = r.Status.ToString(), + r.RequestedAt, + r.ProcessedAt, + ProcessedByUnitName = r.ProcessedByUnit != null ? r.ProcessedByUnit.Name : null, + r.ProcessComments + }) + .ToListAsync(); + + return Ok(requests); + } + + /// + /// 处理申请(同意或拒绝) + /// + [HttpPost("{id}/process")] + public async Task ProcessRequest(int id, [FromBody] ProcessChangeRequestDto request) + { + var unitId = GetCurrentUnitId(); + var userId = GetCurrentUserId(); + if (unitId == null || userId == null) + return Unauthorized(); + + var changeRequest = await _context.ConsumptionReportChangeRequests + .Include(r => r.ConsumptionReport) + .ThenInclude(cr => cr.AllocationDistribution) + .Include(r => r.RequestedByUnit) + .FirstOrDefaultAsync(r => r.Id == id); + + if (changeRequest == null) + return NotFound(new { message = "申请不存在" }); + + if (changeRequest.Status != ChangeRequestStatus.Pending) + return BadRequest(new { message = "该申请已处理" }); + + // 验证是否有权限处理(必须是申请单位的上级) + var isParent = await IsParentUnit(unitId.Value, changeRequest.RequestedByUnitId); + if (!isParent) + return StatusCode(403, new { message = "您没有权限处理此申请" }); + + changeRequest.Status = request.Approved ? ChangeRequestStatus.Approved : ChangeRequestStatus.Rejected; + changeRequest.ProcessedByUnitId = unitId.Value; + changeRequest.ProcessedByUserId = userId.Value; + changeRequest.ProcessedAt = DateTime.UtcNow; + changeRequest.ProcessComments = request.Comments; + + // 如果同意删除,执行删除操作 + if (request.Approved && changeRequest.RequestType == ChangeRequestType.Delete) + { + var report = changeRequest.ConsumptionReport; + var distribution = report.AllocationDistribution; + + // 更新分配的实际完成数量 + distribution.ActualCompletion -= report.ReportedAmount; + if (distribution.ActualCompletion < 0) + distribution.ActualCompletion = 0; + + // 删除消耗记录 + _context.ConsumptionReports.Remove(report); + + _logger.LogInformation("消耗记录 {ReportId} 已被删除,分配 {DistributionId} 的实际完成数量已更新", + report.Id, distribution.Id); + } + + await _context.SaveChangesAsync(); + + var action = request.Approved ? "同意" : "拒绝"; + _logger.LogInformation("单位 {UnitId} {Action}了消耗记录删改申请 {RequestId}", + unitId, action, id); + + return Ok(new { message = $"已{action}该申请" }); + } + + /// + /// 修改消耗记录(上级单位在同意修改申请后执行) + /// + [HttpPost("{id}/modify")] + public async Task ModifyReport(int id, [FromBody] ModifyReportDto request) + { + var unitId = GetCurrentUnitId(); + if (unitId == null) + return Unauthorized(); + + var changeRequest = await _context.ConsumptionReportChangeRequests + .Include(r => r.ConsumptionReport) + .ThenInclude(cr => cr.AllocationDistribution) + .FirstOrDefaultAsync(r => r.Id == id); + + if (changeRequest == null) + return NotFound(new { message = "申请不存在" }); + + if (changeRequest.Status != ChangeRequestStatus.Approved) + return BadRequest(new { message = "只能修改已同意的申请" }); + + if (changeRequest.RequestType != ChangeRequestType.Modify) + return BadRequest(new { message = "该申请不是修改类型" }); + + // 验证是否有权限(必须是处理单位) + if (changeRequest.ProcessedByUnitId != unitId.Value) + return StatusCode(403, new { message = "只有处理单位才能修改" }); + + var report = changeRequest.ConsumptionReport; + var distribution = report.AllocationDistribution; + var oldAmount = report.ReportedAmount; + + // 更新消耗记录 + report.ReportedAmount = request.NewAmount; + report.Remarks = request.NewRemarks ?? report.Remarks; + + // 更新分配的实际完成数量 + distribution.ActualCompletion = distribution.ActualCompletion - oldAmount + request.NewAmount; + + await _context.SaveChangesAsync(); + + _logger.LogInformation("消耗记录 {ReportId} 已被修改,数量从 {OldAmount} 改为 {NewAmount}", + report.Id, oldAmount, request.NewAmount); + + return Ok(new { message = "修改成功" }); + } + + /// + /// 检查消耗记录是否有待处理的申请 + /// + [HttpGet("check/{consumptionReportId}")] + public async Task CheckPendingRequest(int consumptionReportId) + { + var hasPending = await _context.ConsumptionReportChangeRequests + .AnyAsync(r => r.ConsumptionReportId == consumptionReportId + && r.Status == ChangeRequestStatus.Pending); + + return Ok(new { hasPendingRequest = hasPending }); + } + + private async Task> GetSubordinateUnitIds(int unitId) + { + var result = new List(); + var children = await _context.OrganizationalUnits + .Where(u => u.ParentId == unitId) + .Select(u => u.Id) + .ToListAsync(); + + foreach (var childId in children) + { + result.Add(childId); + var grandChildren = await GetSubordinateUnitIds(childId); + result.AddRange(grandChildren); + } + + return result; + } + + private async Task IsParentUnit(int parentUnitId, int childUnitId) + { + var childUnit = await _context.OrganizationalUnits + .FirstOrDefaultAsync(u => u.Id == childUnitId); + + if (childUnit == null) return false; + + var currentParentId = childUnit.ParentId; + while (currentParentId.HasValue) + { + if (currentParentId.Value == parentUnitId) + return true; + + var parent = await _context.OrganizationalUnits + .FirstOrDefaultAsync(u => u.Id == currentParentId.Value); + currentParentId = parent?.ParentId; + } + + return false; + } +} + +public class CreateChangeRequestDto +{ + public int ConsumptionReportId { get; set; } + public ChangeRequestType RequestType { get; set; } + public string Reason { get; set; } = string.Empty; +} + +public class ProcessChangeRequestDto +{ + public bool Approved { get; set; } + public string? Comments { get; set; } +} + +public class ModifyReportDto +{ + public decimal NewAmount { get; set; } + public string? NewRemarks { get; set; } +} diff --git a/src/MilitaryTrainingManagement/Controllers/PersonnelController.cs b/src/MilitaryTrainingManagement/Controllers/PersonnelController.cs index be69084..98e250d 100644 --- a/src/MilitaryTrainingManagement/Controllers/PersonnelController.cs +++ b/src/MilitaryTrainingManagement/Controllers/PersonnelController.cs @@ -551,7 +551,8 @@ public class PersonnelController : BaseApiController public async Task Reject(int id, [FromBody] RejectPersonnelRequest request) { var userId = GetCurrentUserId(); - if (userId == null) + var unitId = GetCurrentUnitId(); + if (userId == null || unitId == null) { return Unauthorized(); } @@ -563,8 +564,8 @@ public class PersonnelController : BaseApiController return StatusCode(403, new { message = "您没有权限拒绝此人员" }); } - var personnel = await _personnelService.RejectAsync(id, userId.Value); - return Ok(personnel); + var personnel = await _personnelService.RejectAsync(id, userId.Value, unitId.Value, request.Reason); + return Ok(MapToResponse(personnel)); } /// @@ -604,7 +605,8 @@ public class PersonnelController : BaseApiController public async Task BatchReject([FromBody] BatchRejectPersonnelRequest request) { var userId = GetCurrentUserId(); - if (userId == null) + var unitId = GetCurrentUnitId(); + if (userId == null || unitId == null) { return Unauthorized(); } @@ -612,7 +614,7 @@ public class PersonnelController : BaseApiController try { var rejectedPersonnel = await _personnelService.BatchRejectPersonnelAsync( - request.PersonnelIds, userId.Value, request.Reason); + request.PersonnelIds, userId.Value, unitId.Value, request.Reason); return Ok(new { rejectedCount = rejectedPersonnel.Count(), @@ -687,7 +689,25 @@ public class PersonnelController : BaseApiController } var history = await _personnelService.GetPersonnelApprovalHistoryAsync(id); - return Ok(history); + + // 映射为简化的响应对象,避免循环引用 + var response = history.Select(h => new + { + h.Id, + h.PersonnelId, + Action = h.Action.ToString(), + PreviousStatus = h.PreviousStatus?.ToString(), + NewStatus = h.NewStatus.ToString(), + PreviousLevel = h.PreviousLevel?.ToString(), + NewLevel = h.NewLevel?.ToString(), + h.ReviewedByUserId, + h.ReviewedByUnitId, + ReviewedByUnitName = h.ReviewedByUnit?.Name, + h.Comments, + h.ReviewedAt + }); + + return Ok(response); } /// diff --git a/src/MilitaryTrainingManagement/Data/ApplicationDbContext.cs b/src/MilitaryTrainingManagement/Data/ApplicationDbContext.cs index cabb249..50faef5 100644 --- a/src/MilitaryTrainingManagement/Data/ApplicationDbContext.cs +++ b/src/MilitaryTrainingManagement/Data/ApplicationDbContext.cs @@ -23,6 +23,7 @@ public class ApplicationDbContext : DbContext public DbSet ApprovalRequests => Set(); public DbSet AuditLogs => Set(); public DbSet ConsumptionReports => Set(); + public DbSet ConsumptionReportChangeRequests => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -205,8 +206,44 @@ public class ApplicationDbContext : DbContext .WithMany() .HasForeignKey(e => e.ReportedByUserId) .OnDelete(DeleteBehavior.NoAction); + entity.HasOne(e => e.ReportedByUnit) + .WithMany() + .HasForeignKey(e => e.ReportedByUnitId) + .OnDelete(DeleteBehavior.NoAction); entity.HasIndex(e => e.AllocationDistributionId); entity.HasIndex(e => e.ReportedAt); + entity.HasIndex(e => e.ReportedByUnitId); + }); + + // ConsumptionReportChangeRequest 配置 + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.Reason).IsRequired().HasMaxLength(500); + entity.Property(e => e.ProcessComments).HasMaxLength(500); + entity.HasOne(e => e.ConsumptionReport) + .WithMany() + .HasForeignKey(e => e.ConsumptionReportId) + .OnDelete(DeleteBehavior.Cascade); + entity.HasOne(e => e.RequestedByUnit) + .WithMany() + .HasForeignKey(e => e.RequestedByUnitId) + .OnDelete(DeleteBehavior.NoAction); + entity.HasOne(e => e.RequestedByUser) + .WithMany() + .HasForeignKey(e => e.RequestedByUserId) + .OnDelete(DeleteBehavior.NoAction); + entity.HasOne(e => e.ProcessedByUnit) + .WithMany() + .HasForeignKey(e => e.ProcessedByUnitId) + .OnDelete(DeleteBehavior.NoAction); + entity.HasOne(e => e.ProcessedByUser) + .WithMany() + .HasForeignKey(e => e.ProcessedByUserId) + .OnDelete(DeleteBehavior.NoAction); + entity.HasIndex(e => e.ConsumptionReportId); + entity.HasIndex(e => e.Status); + entity.HasIndex(e => e.RequestedByUnitId); }); } } diff --git a/src/MilitaryTrainingManagement/Migrations/20260116180324_AddConsumptionReportChangeRequests.Designer.cs b/src/MilitaryTrainingManagement/Migrations/20260116180324_AddConsumptionReportChangeRequests.Designer.cs new file mode 100644 index 0000000..92cb2d8 --- /dev/null +++ b/src/MilitaryTrainingManagement/Migrations/20260116180324_AddConsumptionReportChangeRequests.Designer.cs @@ -0,0 +1,874 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MilitaryTrainingManagement.Data; + +#nullable disable + +namespace MilitaryTrainingManagement.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260116180324_AddConsumptionReportChangeRequests")] + partial class AddConsumptionReportChangeRequests + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("MilitaryTrainingManagement.Models.Entities.AllocationDistribution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ActualCompletion") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("AllocationId") + .HasColumnType("int"); + + b.Property("ApprovedAt") + .HasColumnType("datetime2"); + + b.Property("ApprovedByUserId") + .HasColumnType("int"); + + b.Property("IsApproved") + .HasColumnType("bit"); + + b.Property("ReportedAt") + .HasColumnType("datetime2"); + + b.Property("ReportedByUserId") + .HasColumnType("int"); + + b.Property("TargetUnitId") + .HasColumnType("int"); + + b.Property("UnitQuota") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("AllocationId"); + + b.HasIndex("ApprovedByUserId"); + + b.HasIndex("ReportedByUserId"); + + b.HasIndex("TargetUnitId"); + + b.ToTable("AllocationDistributions"); + }); + + modelBuilder.Entity("MilitaryTrainingManagement.Models.Entities.ApprovalRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("OriginalData") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RequestedAt") + .HasColumnType("datetime2"); + + b.Property("RequestedByUnitId") + .HasColumnType("int"); + + b.Property("RequestedByUserId") + .HasColumnType("int"); + + b.Property("RequestedChanges") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReviewComments") + .HasColumnType("nvarchar(max)"); + + b.Property("ReviewedAt") + .HasColumnType("datetime2"); + + b.Property("ReviewedByUserId") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TargetEntityId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RequestedByUnitId"); + + b.HasIndex("RequestedByUserId"); + + b.HasIndex("ReviewedByUserId"); + + b.ToTable("ApprovalRequests"); + }); + + modelBuilder.Entity("MilitaryTrainingManagement.Models.Entities.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Action") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ChangedFields") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("EntityId") + .HasColumnType("int"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("IsSuccess") + .HasColumnType("bit"); + + b.Property("NewValues") + .HasColumnType("nvarchar(max)"); + + b.Property("OldValues") + .HasColumnType("nvarchar(max)"); + + b.Property("OrganizationalUnitId") + .HasColumnType("int"); + + b.Property("RequestPath") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Timestamp") + .HasColumnType("datetime2"); + + b.Property("UserAgent") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Action"); + + b.HasIndex("EntityId"); + + b.HasIndex("EntityType"); + + b.HasIndex("OrganizationalUnitId"); + + b.HasIndex("Timestamp"); + + b.HasIndex("UserId"); + + b.ToTable("AuditLogs"); + }); + + modelBuilder.Entity("MilitaryTrainingManagement.Models.Entities.ConsumptionReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AllocationDistributionId") + .HasColumnType("int"); + + b.Property("CumulativeAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Remarks") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ReportedAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ReportedAt") + .HasColumnType("datetime2"); + + b.Property("ReportedByUnitId") + .HasColumnType("int"); + + b.Property("ReportedByUserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AllocationDistributionId"); + + b.HasIndex("ReportedAt"); + + b.HasIndex("ReportedByUnitId"); + + b.HasIndex("ReportedByUserId"); + + b.ToTable("ConsumptionReports"); + }); + + modelBuilder.Entity("MilitaryTrainingManagement.Models.Entities.ConsumptionReportChangeRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConsumptionReportId") + .HasColumnType("int"); + + b.Property("ProcessComments") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ProcessedAt") + .HasColumnType("datetime2"); + + b.Property("ProcessedByUnitId") + .HasColumnType("int"); + + b.Property("ProcessedByUserId") + .HasColumnType("int"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RequestType") + .HasColumnType("int"); + + b.Property("RequestedAt") + .HasColumnType("datetime2"); + + b.Property("RequestedByUnitId") + .HasColumnType("int"); + + b.Property("RequestedByUserId") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ConsumptionReportId"); + + b.HasIndex("ProcessedByUnitId"); + + b.HasIndex("ProcessedByUserId"); + + b.HasIndex("RequestedByUnitId"); + + b.HasIndex("RequestedByUserId"); + + b.HasIndex("Status"); + + b.ToTable("ConsumptionReportChangeRequests"); + }); + + modelBuilder.Entity("MilitaryTrainingManagement.Models.Entities.MaterialAllocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedByUnitId") + .HasColumnType("int"); + + b.Property("MaterialName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("TotalQuota") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUnitId"); + + b.ToTable("MaterialAllocations"); + }); + + modelBuilder.Entity("MilitaryTrainingManagement.Models.Entities.MaterialCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("MaterialCategories"); + }); + + modelBuilder.Entity("MilitaryTrainingManagement.Models.Entities.OrganizationalUnit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ParentId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("OrganizationalUnits"); + }); + + modelBuilder.Entity("MilitaryTrainingManagement.Models.Entities.Personnel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Achievements") + .HasColumnType("nvarchar(max)"); + + b.Property("Age") + .HasColumnType("int"); + + b.Property("ApprovedAt") + .HasColumnType("datetime2"); + + b.Property("ApprovedByUnitId") + .HasColumnType("int"); + + b.Property("ApprovedLevel") + .HasColumnType("int"); + + b.Property("BirthDate") + .HasColumnType("nvarchar(max)"); + + b.Property("ContactInfo") + .HasColumnType("nvarchar(max)"); + + b.Property("EducationLevel") + .HasColumnType("nvarchar(max)"); + + b.Property("EnlistmentDate") + .HasColumnType("nvarchar(max)"); + + b.Property("Ethnicity") + .HasColumnType("nvarchar(max)"); + + b.Property("Gender") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("Height") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("Hometown") + .HasColumnType("nvarchar(max)"); + + b.Property("IdNumber") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("PendingUpgradeByUnitId") + .HasColumnType("int"); + + b.Property("PhotoPath") + .HasColumnType("nvarchar(max)"); + + b.Property("PoliticalStatus") + .HasColumnType("nvarchar(max)"); + + b.Property("Position") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ProfessionalTitle") + .HasColumnType("nvarchar(max)"); + + b.Property("Rank") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Specialty") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SubmittedAt") + .HasColumnType("datetime2"); + + b.Property("SubmittedByUnitId") + .HasColumnType("int"); + + b.Property("SupportingDocuments") + .HasColumnType("nvarchar(max)"); + + b.Property("TrainingParticipation") + .HasColumnType("nvarchar(max)"); + + b.Property("Unit") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ApprovedByUnitId"); + + b.HasIndex("PendingUpgradeByUnitId"); + + b.HasIndex("SubmittedByUnitId"); + + b.ToTable("Personnel"); + }); + + modelBuilder.Entity("MilitaryTrainingManagement.Models.Entities.PersonnelApprovalHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Action") + .HasColumnType("int"); + + b.Property("Comments") + .HasColumnType("nvarchar(max)"); + + b.Property("NewLevel") + .HasColumnType("int"); + + b.Property("NewStatus") + .HasColumnType("int"); + + b.Property("PersonnelId") + .HasColumnType("int"); + + b.Property("PreviousLevel") + .HasColumnType("int"); + + b.Property("PreviousStatus") + .HasColumnType("int"); + + b.Property("ReviewedAt") + .HasColumnType("datetime2"); + + b.Property("ReviewedByUnitId") + .HasColumnType("int"); + + b.Property("ReviewedByUserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("PersonnelId"); + + b.HasIndex("ReviewedByUnitId"); + + b.HasIndex("ReviewedByUserId"); + + b.ToTable("PersonnelApprovalHistories"); + }); + + modelBuilder.Entity("MilitaryTrainingManagement.Models.Entities.UserAccount", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("LastLoginAt") + .HasColumnType("datetime2"); + + b.Property("OrganizationalUnitId") + .HasColumnType("int"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlainPassword") + .HasColumnType("nvarchar(max)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationalUnitId"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("UserAccounts"); + }); + + modelBuilder.Entity("MilitaryTrainingManagement.Models.Entities.AllocationDistribution", b => + { + b.HasOne("MilitaryTrainingManagement.Models.Entities.MaterialAllocation", "Allocation") + .WithMany("Distributions") + .HasForeignKey("AllocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MilitaryTrainingManagement.Models.Entities.UserAccount", "ApprovedByUser") + .WithMany() + .HasForeignKey("ApprovedByUserId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("MilitaryTrainingManagement.Models.Entities.UserAccount", "ReportedByUser") + .WithMany() + .HasForeignKey("ReportedByUserId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("MilitaryTrainingManagement.Models.Entities.OrganizationalUnit", "TargetUnit") + .WithMany() + .HasForeignKey("TargetUnitId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Allocation"); + + b.Navigation("ApprovedByUser"); + + b.Navigation("ReportedByUser"); + + b.Navigation("TargetUnit"); + }); + + modelBuilder.Entity("MilitaryTrainingManagement.Models.Entities.ApprovalRequest", b => + { + b.HasOne("MilitaryTrainingManagement.Models.Entities.OrganizationalUnit", "RequestedByUnit") + .WithMany() + .HasForeignKey("RequestedByUnitId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("MilitaryTrainingManagement.Models.Entities.UserAccount", "RequestedByUser") + .WithMany() + .HasForeignKey("RequestedByUserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("MilitaryTrainingManagement.Models.Entities.UserAccount", "ReviewedByUser") + .WithMany() + .HasForeignKey("ReviewedByUserId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("RequestedByUnit"); + + b.Navigation("RequestedByUser"); + + b.Navigation("ReviewedByUser"); + }); + + modelBuilder.Entity("MilitaryTrainingManagement.Models.Entities.AuditLog", b => + { + b.HasOne("MilitaryTrainingManagement.Models.Entities.OrganizationalUnit", "OrganizationalUnit") + .WithMany() + .HasForeignKey("OrganizationalUnitId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("MilitaryTrainingManagement.Models.Entities.UserAccount", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("OrganizationalUnit"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MilitaryTrainingManagement.Models.Entities.ConsumptionReport", b => + { + b.HasOne("MilitaryTrainingManagement.Models.Entities.AllocationDistribution", "AllocationDistribution") + .WithMany() + .HasForeignKey("AllocationDistributionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MilitaryTrainingManagement.Models.Entities.OrganizationalUnit", "ReportedByUnit") + .WithMany() + .HasForeignKey("ReportedByUnitId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("MilitaryTrainingManagement.Models.Entities.UserAccount", "ReportedByUser") + .WithMany() + .HasForeignKey("ReportedByUserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("AllocationDistribution"); + + b.Navigation("ReportedByUnit"); + + b.Navigation("ReportedByUser"); + }); + + modelBuilder.Entity("MilitaryTrainingManagement.Models.Entities.ConsumptionReportChangeRequest", b => + { + b.HasOne("MilitaryTrainingManagement.Models.Entities.ConsumptionReport", "ConsumptionReport") + .WithMany() + .HasForeignKey("ConsumptionReportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MilitaryTrainingManagement.Models.Entities.OrganizationalUnit", "ProcessedByUnit") + .WithMany() + .HasForeignKey("ProcessedByUnitId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("MilitaryTrainingManagement.Models.Entities.UserAccount", "ProcessedByUser") + .WithMany() + .HasForeignKey("ProcessedByUserId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("MilitaryTrainingManagement.Models.Entities.OrganizationalUnit", "RequestedByUnit") + .WithMany() + .HasForeignKey("RequestedByUnitId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("MilitaryTrainingManagement.Models.Entities.UserAccount", "RequestedByUser") + .WithMany() + .HasForeignKey("RequestedByUserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("ConsumptionReport"); + + b.Navigation("ProcessedByUnit"); + + b.Navigation("ProcessedByUser"); + + b.Navigation("RequestedByUnit"); + + b.Navigation("RequestedByUser"); + }); + + modelBuilder.Entity("MilitaryTrainingManagement.Models.Entities.MaterialAllocation", b => + { + b.HasOne("MilitaryTrainingManagement.Models.Entities.OrganizationalUnit", "CreatedByUnit") + .WithMany() + .HasForeignKey("CreatedByUnitId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByUnit"); + }); + + modelBuilder.Entity("MilitaryTrainingManagement.Models.Entities.OrganizationalUnit", b => + { + b.HasOne("MilitaryTrainingManagement.Models.Entities.OrganizationalUnit", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("MilitaryTrainingManagement.Models.Entities.Personnel", b => + { + b.HasOne("MilitaryTrainingManagement.Models.Entities.OrganizationalUnit", "ApprovedByUnit") + .WithMany() + .HasForeignKey("ApprovedByUnitId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("MilitaryTrainingManagement.Models.Entities.OrganizationalUnit", "PendingUpgradeByUnit") + .WithMany() + .HasForeignKey("PendingUpgradeByUnitId"); + + b.HasOne("MilitaryTrainingManagement.Models.Entities.OrganizationalUnit", "SubmittedByUnit") + .WithMany() + .HasForeignKey("SubmittedByUnitId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("ApprovedByUnit"); + + b.Navigation("PendingUpgradeByUnit"); + + b.Navigation("SubmittedByUnit"); + }); + + modelBuilder.Entity("MilitaryTrainingManagement.Models.Entities.PersonnelApprovalHistory", b => + { + b.HasOne("MilitaryTrainingManagement.Models.Entities.Personnel", "Personnel") + .WithMany() + .HasForeignKey("PersonnelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MilitaryTrainingManagement.Models.Entities.OrganizationalUnit", "ReviewedByUnit") + .WithMany() + .HasForeignKey("ReviewedByUnitId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("MilitaryTrainingManagement.Models.Entities.UserAccount", "ReviewedByUser") + .WithMany() + .HasForeignKey("ReviewedByUserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Personnel"); + + b.Navigation("ReviewedByUnit"); + + b.Navigation("ReviewedByUser"); + }); + + modelBuilder.Entity("MilitaryTrainingManagement.Models.Entities.UserAccount", b => + { + b.HasOne("MilitaryTrainingManagement.Models.Entities.OrganizationalUnit", "OrganizationalUnit") + .WithMany("Accounts") + .HasForeignKey("OrganizationalUnitId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("OrganizationalUnit"); + }); + + modelBuilder.Entity("MilitaryTrainingManagement.Models.Entities.MaterialAllocation", b => + { + b.Navigation("Distributions"); + }); + + modelBuilder.Entity("MilitaryTrainingManagement.Models.Entities.OrganizationalUnit", b => + { + b.Navigation("Accounts"); + + b.Navigation("Children"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/MilitaryTrainingManagement/Migrations/20260116180324_AddConsumptionReportChangeRequests.cs b/src/MilitaryTrainingManagement/Migrations/20260116180324_AddConsumptionReportChangeRequests.cs new file mode 100644 index 0000000..e9c9bcb --- /dev/null +++ b/src/MilitaryTrainingManagement/Migrations/20260116180324_AddConsumptionReportChangeRequests.cs @@ -0,0 +1,162 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MilitaryTrainingManagement.Migrations +{ + /// + public partial class AddConsumptionReportChangeRequests : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PendingUpgradeByUnitId", + table: "Personnel", + type: "int", + nullable: true); + + migrationBuilder.AddColumn( + name: "ReportedByUnitId", + table: "ConsumptionReports", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.CreateTable( + name: "ConsumptionReportChangeRequests", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + ConsumptionReportId = table.Column(type: "int", nullable: false), + RequestType = table.Column(type: "int", nullable: false), + Reason = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: false), + Status = table.Column(type: "int", nullable: false), + RequestedByUnitId = table.Column(type: "int", nullable: false), + RequestedByUserId = table.Column(type: "int", nullable: false), + RequestedAt = table.Column(type: "datetime2", nullable: false), + ProcessedByUnitId = table.Column(type: "int", nullable: true), + ProcessedByUserId = table.Column(type: "int", nullable: true), + ProcessedAt = table.Column(type: "datetime2", nullable: true), + ProcessComments = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ConsumptionReportChangeRequests", x => x.Id); + table.ForeignKey( + name: "FK_ConsumptionReportChangeRequests_ConsumptionReports_ConsumptionReportId", + column: x => x.ConsumptionReportId, + principalTable: "ConsumptionReports", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ConsumptionReportChangeRequests_OrganizationalUnits_ProcessedByUnitId", + column: x => x.ProcessedByUnitId, + principalTable: "OrganizationalUnits", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_ConsumptionReportChangeRequests_OrganizationalUnits_RequestedByUnitId", + column: x => x.RequestedByUnitId, + principalTable: "OrganizationalUnits", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_ConsumptionReportChangeRequests_UserAccounts_ProcessedByUserId", + column: x => x.ProcessedByUserId, + principalTable: "UserAccounts", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_ConsumptionReportChangeRequests_UserAccounts_RequestedByUserId", + column: x => x.RequestedByUserId, + principalTable: "UserAccounts", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_Personnel_PendingUpgradeByUnitId", + table: "Personnel", + column: "PendingUpgradeByUnitId"); + + migrationBuilder.CreateIndex( + name: "IX_ConsumptionReports_ReportedByUnitId", + table: "ConsumptionReports", + column: "ReportedByUnitId"); + + migrationBuilder.CreateIndex( + name: "IX_ConsumptionReportChangeRequests_ConsumptionReportId", + table: "ConsumptionReportChangeRequests", + column: "ConsumptionReportId"); + + migrationBuilder.CreateIndex( + name: "IX_ConsumptionReportChangeRequests_ProcessedByUnitId", + table: "ConsumptionReportChangeRequests", + column: "ProcessedByUnitId"); + + migrationBuilder.CreateIndex( + name: "IX_ConsumptionReportChangeRequests_ProcessedByUserId", + table: "ConsumptionReportChangeRequests", + column: "ProcessedByUserId"); + + migrationBuilder.CreateIndex( + name: "IX_ConsumptionReportChangeRequests_RequestedByUnitId", + table: "ConsumptionReportChangeRequests", + column: "RequestedByUnitId"); + + migrationBuilder.CreateIndex( + name: "IX_ConsumptionReportChangeRequests_RequestedByUserId", + table: "ConsumptionReportChangeRequests", + column: "RequestedByUserId"); + + migrationBuilder.CreateIndex( + name: "IX_ConsumptionReportChangeRequests_Status", + table: "ConsumptionReportChangeRequests", + column: "Status"); + + migrationBuilder.AddForeignKey( + name: "FK_ConsumptionReports_OrganizationalUnits_ReportedByUnitId", + table: "ConsumptionReports", + column: "ReportedByUnitId", + principalTable: "OrganizationalUnits", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Personnel_OrganizationalUnits_PendingUpgradeByUnitId", + table: "Personnel", + column: "PendingUpgradeByUnitId", + principalTable: "OrganizationalUnits", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_ConsumptionReports_OrganizationalUnits_ReportedByUnitId", + table: "ConsumptionReports"); + + migrationBuilder.DropForeignKey( + name: "FK_Personnel_OrganizationalUnits_PendingUpgradeByUnitId", + table: "Personnel"); + + migrationBuilder.DropTable( + name: "ConsumptionReportChangeRequests"); + + migrationBuilder.DropIndex( + name: "IX_Personnel_PendingUpgradeByUnitId", + table: "Personnel"); + + migrationBuilder.DropIndex( + name: "IX_ConsumptionReports_ReportedByUnitId", + table: "ConsumptionReports"); + + migrationBuilder.DropColumn( + name: "PendingUpgradeByUnitId", + table: "Personnel"); + + migrationBuilder.DropColumn( + name: "ReportedByUnitId", + table: "ConsumptionReports"); + } + } +} diff --git a/src/MilitaryTrainingManagement/Migrations/ApplicationDbContextModelSnapshot.cs b/src/MilitaryTrainingManagement/Migrations/ApplicationDbContextModelSnapshot.cs index af40d11..2bdd2e1 100644 --- a/src/MilitaryTrainingManagement/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/MilitaryTrainingManagement/Migrations/ApplicationDbContextModelSnapshot.cs @@ -236,6 +236,9 @@ namespace MilitaryTrainingManagement.Migrations b.Property("ReportedAt") .HasColumnType("datetime2"); + b.Property("ReportedByUnitId") + .HasColumnType("int"); + b.Property("ReportedByUserId") .HasColumnType("int"); @@ -245,11 +248,74 @@ namespace MilitaryTrainingManagement.Migrations b.HasIndex("ReportedAt"); + b.HasIndex("ReportedByUnitId"); + b.HasIndex("ReportedByUserId"); b.ToTable("ConsumptionReports"); }); + modelBuilder.Entity("MilitaryTrainingManagement.Models.Entities.ConsumptionReportChangeRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConsumptionReportId") + .HasColumnType("int"); + + b.Property("ProcessComments") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ProcessedAt") + .HasColumnType("datetime2"); + + b.Property("ProcessedByUnitId") + .HasColumnType("int"); + + b.Property("ProcessedByUserId") + .HasColumnType("int"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RequestType") + .HasColumnType("int"); + + b.Property("RequestedAt") + .HasColumnType("datetime2"); + + b.Property("RequestedByUnitId") + .HasColumnType("int"); + + b.Property("RequestedByUserId") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ConsumptionReportId"); + + b.HasIndex("ProcessedByUnitId"); + + b.HasIndex("ProcessedByUserId"); + + b.HasIndex("RequestedByUnitId"); + + b.HasIndex("RequestedByUserId"); + + b.HasIndex("Status"); + + b.ToTable("ConsumptionReportChangeRequests"); + }); + modelBuilder.Entity("MilitaryTrainingManagement.Models.Entities.MaterialAllocation", b => { b.Property("Id") @@ -412,6 +478,9 @@ namespace MilitaryTrainingManagement.Migrations .HasMaxLength(50) .HasColumnType("nvarchar(50)"); + b.Property("PendingUpgradeByUnitId") + .HasColumnType("int"); + b.Property("PhotoPath") .HasColumnType("nvarchar(max)"); @@ -456,6 +525,8 @@ namespace MilitaryTrainingManagement.Migrations b.HasIndex("ApprovedByUnitId"); + b.HasIndex("PendingUpgradeByUnitId"); + b.HasIndex("SubmittedByUnitId"); b.ToTable("Personnel"); @@ -641,6 +712,12 @@ namespace MilitaryTrainingManagement.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.HasOne("MilitaryTrainingManagement.Models.Entities.OrganizationalUnit", "ReportedByUnit") + .WithMany() + .HasForeignKey("ReportedByUnitId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + b.HasOne("MilitaryTrainingManagement.Models.Entities.UserAccount", "ReportedByUser") .WithMany() .HasForeignKey("ReportedByUserId") @@ -649,9 +726,52 @@ namespace MilitaryTrainingManagement.Migrations b.Navigation("AllocationDistribution"); + b.Navigation("ReportedByUnit"); + b.Navigation("ReportedByUser"); }); + modelBuilder.Entity("MilitaryTrainingManagement.Models.Entities.ConsumptionReportChangeRequest", b => + { + b.HasOne("MilitaryTrainingManagement.Models.Entities.ConsumptionReport", "ConsumptionReport") + .WithMany() + .HasForeignKey("ConsumptionReportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MilitaryTrainingManagement.Models.Entities.OrganizationalUnit", "ProcessedByUnit") + .WithMany() + .HasForeignKey("ProcessedByUnitId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("MilitaryTrainingManagement.Models.Entities.UserAccount", "ProcessedByUser") + .WithMany() + .HasForeignKey("ProcessedByUserId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("MilitaryTrainingManagement.Models.Entities.OrganizationalUnit", "RequestedByUnit") + .WithMany() + .HasForeignKey("RequestedByUnitId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("MilitaryTrainingManagement.Models.Entities.UserAccount", "RequestedByUser") + .WithMany() + .HasForeignKey("RequestedByUserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("ConsumptionReport"); + + b.Navigation("ProcessedByUnit"); + + b.Navigation("ProcessedByUser"); + + b.Navigation("RequestedByUnit"); + + b.Navigation("RequestedByUser"); + }); + modelBuilder.Entity("MilitaryTrainingManagement.Models.Entities.MaterialAllocation", b => { b.HasOne("MilitaryTrainingManagement.Models.Entities.OrganizationalUnit", "CreatedByUnit") @@ -680,6 +800,10 @@ namespace MilitaryTrainingManagement.Migrations .HasForeignKey("ApprovedByUnitId") .OnDelete(DeleteBehavior.NoAction); + b.HasOne("MilitaryTrainingManagement.Models.Entities.OrganizationalUnit", "PendingUpgradeByUnit") + .WithMany() + .HasForeignKey("PendingUpgradeByUnitId"); + b.HasOne("MilitaryTrainingManagement.Models.Entities.OrganizationalUnit", "SubmittedByUnit") .WithMany() .HasForeignKey("SubmittedByUnitId") @@ -688,6 +812,8 @@ namespace MilitaryTrainingManagement.Migrations b.Navigation("ApprovedByUnit"); + b.Navigation("PendingUpgradeByUnit"); + b.Navigation("SubmittedByUnit"); }); diff --git a/src/MilitaryTrainingManagement/Models/Entities/ConsumptionReport.cs b/src/MilitaryTrainingManagement/Models/Entities/ConsumptionReport.cs index fa0f11c..da636e0 100644 --- a/src/MilitaryTrainingManagement/Models/Entities/ConsumptionReport.cs +++ b/src/MilitaryTrainingManagement/Models/Entities/ConsumptionReport.cs @@ -23,6 +23,12 @@ public class ConsumptionReport [ForeignKey(nameof(AllocationDistributionId))] public AllocationDistribution AllocationDistribution { get; set; } = null!; + /// + /// 配额分配(别名,用于兼容) + /// + [NotMapped] + public AllocationDistribution Distribution => AllocationDistribution; + /// /// 本次上报数量 /// @@ -55,6 +61,18 @@ public class ConsumptionReport [ForeignKey(nameof(ReportedByUserId))] public UserAccount ReportedByUser { get; set; } = null!; + /// + /// 上报单位ID + /// + [Required] + public int ReportedByUnitId { get; set; } + + /// + /// 上报单位 + /// + [ForeignKey(nameof(ReportedByUnitId))] + public OrganizationalUnit ReportedByUnit { get; set; } = null!; + /// /// 上报时间 /// diff --git a/src/MilitaryTrainingManagement/Models/Entities/ConsumptionReportChangeRequest.cs b/src/MilitaryTrainingManagement/Models/Entities/ConsumptionReportChangeRequest.cs new file mode 100644 index 0000000..4afba05 --- /dev/null +++ b/src/MilitaryTrainingManagement/Models/Entities/ConsumptionReportChangeRequest.cs @@ -0,0 +1,91 @@ +using MilitaryTrainingManagement.Models.Enums; + +namespace MilitaryTrainingManagement.Models.Entities; + +/// +/// 消耗记录删改申请 +/// +public class ConsumptionReportChangeRequest +{ + public int Id { get; set; } + + /// + /// 关联的消耗记录ID + /// + public int ConsumptionReportId { get; set; } + + /// + /// 关联的消耗记录 + /// + public ConsumptionReport ConsumptionReport { get; set; } = null!; + + /// + /// 申请类型:Delete(删除)或 Modify(修改) + /// + public ChangeRequestType RequestType { get; set; } + + /// + /// 申请原因 + /// + public string Reason { get; set; } = string.Empty; + + /// + /// 申请状态 + /// + public ChangeRequestStatus Status { get; set; } = ChangeRequestStatus.Pending; + + /// + /// 申请单位ID + /// + public int RequestedByUnitId { get; set; } + + /// + /// 申请单位 + /// + public OrganizationalUnit RequestedByUnit { get; set; } = null!; + + /// + /// 申请人用户ID + /// + public int RequestedByUserId { get; set; } + + /// + /// 申请人 + /// + public UserAccount RequestedByUser { get; set; } = null!; + + /// + /// 申请时间 + /// + public DateTime RequestedAt { get; set; } = DateTime.UtcNow; + + /// + /// 处理单位ID + /// + public int? ProcessedByUnitId { get; set; } + + /// + /// 处理单位 + /// + public OrganizationalUnit? ProcessedByUnit { get; set; } + + /// + /// 处理人用户ID + /// + public int? ProcessedByUserId { get; set; } + + /// + /// 处理人 + /// + public UserAccount? ProcessedByUser { get; set; } + + /// + /// 处理时间 + /// + public DateTime? ProcessedAt { get; set; } + + /// + /// 处理意见 + /// + public string? ProcessComments { get; set; } +} diff --git a/src/MilitaryTrainingManagement/Models/Enums/ChangeRequestStatus.cs b/src/MilitaryTrainingManagement/Models/Enums/ChangeRequestStatus.cs new file mode 100644 index 0000000..15acec6 --- /dev/null +++ b/src/MilitaryTrainingManagement/Models/Enums/ChangeRequestStatus.cs @@ -0,0 +1,14 @@ +namespace MilitaryTrainingManagement.Models.Enums; + +/// +/// 删改申请状态 +/// +public enum ChangeRequestStatus +{ + /// 待处理 + Pending = 1, + /// 已同意 + Approved = 2, + /// 已拒绝 + Rejected = 3 +} diff --git a/src/MilitaryTrainingManagement/Models/Enums/ChangeRequestType.cs b/src/MilitaryTrainingManagement/Models/Enums/ChangeRequestType.cs new file mode 100644 index 0000000..8fb7c32 --- /dev/null +++ b/src/MilitaryTrainingManagement/Models/Enums/ChangeRequestType.cs @@ -0,0 +1,12 @@ +namespace MilitaryTrainingManagement.Models.Enums; + +/// +/// 删改申请类型 +/// +public enum ChangeRequestType +{ + /// 删除 + Delete = 1, + /// 修改 + Modify = 2 +} diff --git a/src/MilitaryTrainingManagement/Program.cs b/src/MilitaryTrainingManagement/Program.cs index 781d59d..74dc889 100644 --- a/src/MilitaryTrainingManagement/Program.cs +++ b/src/MilitaryTrainingManagement/Program.cs @@ -293,6 +293,62 @@ using (var scope = app.Services.CreateScope()) Console.WriteLine($"创建 ConsumptionReports 表时出错: {ex.Message}"); } + // 添加 ConsumptionReports.ReportedByUnitId 列(如果不存在) + try + { + context.Database.ExecuteSqlRaw(@" + IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('ConsumptionReports') AND name = 'ReportedByUnitId') + ALTER TABLE ConsumptionReports ADD ReportedByUnitId INT NOT NULL DEFAULT 1; + "); + Console.WriteLine("ConsumptionReports.ReportedByUnitId 列检查完成"); + } + catch (Exception ex) + { + Console.WriteLine($"添加 ConsumptionReports.ReportedByUnitId 列时出错: {ex.Message}"); + } + + // 创建 ConsumptionReportChangeRequests 表(如果不存在) + try + { + context.Database.ExecuteSqlRaw(@" + IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'ConsumptionReportChangeRequests') + BEGIN + CREATE TABLE ConsumptionReportChangeRequests ( + Id INT IDENTITY(1,1) PRIMARY KEY, + ConsumptionReportId INT NOT NULL, + RequestType INT NOT NULL, + Reason NVARCHAR(500) NOT NULL, + Status INT NOT NULL, + RequestedByUnitId INT NOT NULL, + RequestedByUserId INT NOT NULL, + RequestedAt DATETIME2 NOT NULL, + ProcessedByUnitId INT NULL, + ProcessedByUserId INT NULL, + ProcessedAt DATETIME2 NULL, + ProcessComments NVARCHAR(500) NULL, + CONSTRAINT FK_ConsumptionReportChangeRequests_ConsumptionReports + FOREIGN KEY (ConsumptionReportId) REFERENCES ConsumptionReports(Id) ON DELETE CASCADE, + CONSTRAINT FK_ConsumptionReportChangeRequests_RequestedByUnit + FOREIGN KEY (RequestedByUnitId) REFERENCES OrganizationalUnits(Id), + CONSTRAINT FK_ConsumptionReportChangeRequests_RequestedByUser + FOREIGN KEY (RequestedByUserId) REFERENCES UserAccounts(Id), + CONSTRAINT FK_ConsumptionReportChangeRequests_ProcessedByUnit + FOREIGN KEY (ProcessedByUnitId) REFERENCES OrganizationalUnits(Id), + CONSTRAINT FK_ConsumptionReportChangeRequests_ProcessedByUser + FOREIGN KEY (ProcessedByUserId) REFERENCES UserAccounts(Id) + ); + CREATE INDEX IX_ConsumptionReportChangeRequests_ConsumptionReportId ON ConsumptionReportChangeRequests(ConsumptionReportId); + CREATE INDEX IX_ConsumptionReportChangeRequests_Status ON ConsumptionReportChangeRequests(Status); + CREATE INDEX IX_ConsumptionReportChangeRequests_RequestedByUnitId ON ConsumptionReportChangeRequests(RequestedByUnitId); + END + "); + Console.WriteLine("ConsumptionReportChangeRequests 表检查完成"); + } + catch (Exception ex) + { + Console.WriteLine($"创建 ConsumptionReportChangeRequests 表时出错: {ex.Message}"); + } + // 如果没有物资类别,创建默认类别 if (!context.MaterialCategories.Any()) { diff --git a/src/MilitaryTrainingManagement/Services/Implementations/AllocationService.cs b/src/MilitaryTrainingManagement/Services/Implementations/AllocationService.cs index c2d8ec9..28418dc 100644 --- a/src/MilitaryTrainingManagement/Services/Implementations/AllocationService.cs +++ b/src/MilitaryTrainingManagement/Services/Implementations/AllocationService.cs @@ -316,6 +316,7 @@ public class AllocationService : IAllocationService CumulativeAmount = actualCompletion, Remarks = remarks, ReportedByUserId = userId, + ReportedByUnitId = unitId, ReportedAt = DateTime.UtcNow }; _context.ConsumptionReports.Add(consumptionReport); diff --git a/src/MilitaryTrainingManagement/Services/Implementations/PersonnelService.cs b/src/MilitaryTrainingManagement/Services/Implementations/PersonnelService.cs index 8779777..bde22a7 100644 --- a/src/MilitaryTrainingManagement/Services/Implementations/PersonnelService.cs +++ b/src/MilitaryTrainingManagement/Services/Implementations/PersonnelService.cs @@ -179,7 +179,7 @@ public class PersonnelService : IPersonnelService }; } - public async Task RejectAsync(int personnelId, int reviewedByUserId) + public async Task RejectAsync(int personnelId, int reviewedByUserId, int reviewedByUnitId, string? comments = null) { var personnel = await _context.Personnel.FindAsync(personnelId); if (personnel == null) @@ -193,7 +193,7 @@ public class PersonnelService : IPersonnelService // 记录审批历史 await RecordApprovalHistoryAsync(personnelId, PersonnelApprovalAction.Rejected, previousStatus, PersonnelStatus.Rejected, null, null, - reviewedByUserId, null, "审批拒绝"); + reviewedByUserId, reviewedByUnitId, comments ?? "审批拒绝"); _logger.LogInformation("人员 {PersonnelId} 已被用户 {UserId} 拒绝", personnelId, reviewedByUserId); @@ -532,7 +532,7 @@ public class PersonnelService : IPersonnelService return approvedPersonnel; } - public async Task> BatchRejectPersonnelAsync(int[] personnelIds, int reviewedByUserId, string reason) + public async Task> BatchRejectPersonnelAsync(int[] personnelIds, int reviewedByUserId, int reviewedByUnitId, string reason) { var rejectedPersonnel = new List(); @@ -540,13 +540,8 @@ public class PersonnelService : IPersonnelService { try { - var personnel = await RejectAsync(personnelId, reviewedByUserId); + var personnel = await RejectAsync(personnelId, reviewedByUserId, reviewedByUnitId, reason); rejectedPersonnel.Add(personnel); - - // 记录审批历史 - await RecordApprovalHistoryAsync(personnelId, PersonnelApprovalAction.Rejected, - PersonnelStatus.Pending, PersonnelStatus.Rejected, null, null, - reviewedByUserId, null, reason); } catch (Exception ex) { diff --git a/src/MilitaryTrainingManagement/Services/Interfaces/IPersonnelService.cs b/src/MilitaryTrainingManagement/Services/Interfaces/IPersonnelService.cs index 3fd5dab..bc7b339 100644 --- a/src/MilitaryTrainingManagement/Services/Interfaces/IPersonnelService.cs +++ b/src/MilitaryTrainingManagement/Services/Interfaces/IPersonnelService.cs @@ -13,7 +13,7 @@ public interface IPersonnelService Task CreateAsync(Personnel personnel); Task UpdateAsync(Personnel personnel); Task ApproveAsync(int personnelId, int approvedByUnitId, PersonnelLevel? level = null); - Task RejectAsync(int personnelId, int reviewedByUserId); + Task RejectAsync(int personnelId, int reviewedByUserId, int reviewedByUnitId, string? comments = null); Task DeleteAsync(int id); /// @@ -64,7 +64,7 @@ public interface IPersonnelService /// /// 批量拒绝人员 /// - Task> BatchRejectPersonnelAsync(int[] personnelIds, int reviewedByUserId, string reason); + Task> BatchRejectPersonnelAsync(int[] personnelIds, int reviewedByUserId, int reviewedByUnitId, string reason); /// /// 获取人员审批历史 diff --git a/src/frontend/src/api/allocations.ts b/src/frontend/src/api/allocations.ts index ce2ea60..1148b24 100644 --- a/src/frontend/src/api/allocations.ts +++ b/src/frontend/src/api/allocations.ts @@ -65,6 +65,7 @@ export interface ConsumptionReport { cumulativeAmount: number remarks?: string reportedByUserName?: string + reportedByUnitId?: number reportedAt: string } @@ -76,3 +77,68 @@ export interface UnitReportSummary { reportCount: number lastReportedAt?: string } + +// 消耗记录删改申请相关 +export interface ChangeRequest { + id: number + consumptionReportId: number + requestType: 'Delete' | 'Modify' + reason: string + status: 'Pending' | 'Approved' | 'Rejected' + requestedAt: string + requestedByUnitName?: string + requestedByUserName?: string + processedAt?: string + processedByUnitName?: string + processComments?: string + consumptionReport?: { + id: number + reportedAmount: number + reportedAt: string + remarks?: string + materialName?: string + unit?: string + } +} + +export const changeRequestsApi = { + // 创建删改申请 + async create(data: { consumptionReportId: number; requestType: 'Delete' | 'Modify'; reason: string }): Promise<{ message: string; id: number }> { + const response = await apiClient.post<{ message: string; id: number }>('/ConsumptionChangeRequests', { + consumptionReportId: data.consumptionReportId, + requestType: data.requestType === 'Delete' ? 1 : 2, + reason: data.reason + }) + return response.data + }, + + // 获取待处理的申请(上级查看下级的) + async getPending(): Promise { + const response = await apiClient.get('/ConsumptionChangeRequests/pending') + return response.data + }, + + // 获取本单位的申请 + async getMy(): Promise { + const response = await apiClient.get('/ConsumptionChangeRequests/my') + return response.data + }, + + // 处理申请 + async process(id: number, data: { approved: boolean; comments?: string }): Promise<{ message: string }> { + const response = await apiClient.post<{ message: string }>(`/ConsumptionChangeRequests/${id}/process`, data) + return response.data + }, + + // 修改消耗记录 + async modify(id: number, data: { newAmount: number; newRemarks?: string }): Promise<{ message: string }> { + const response = await apiClient.post<{ message: string }>(`/ConsumptionChangeRequests/${id}/modify`, data) + return response.data + }, + + // 检查是否有待处理的申请 + async checkPending(consumptionReportId: number): Promise<{ hasPendingRequest: boolean }> { + const response = await apiClient.get<{ hasPendingRequest: boolean }>(`/ConsumptionChangeRequests/check/${consumptionReportId}`) + return response.data + } +} diff --git a/src/frontend/src/api/index.ts b/src/frontend/src/api/index.ts index 285ee00..13353ad 100644 --- a/src/frontend/src/api/index.ts +++ b/src/frontend/src/api/index.ts @@ -1,6 +1,6 @@ export { authApi } from './auth' export { organizationsApi } from './organizations' -export { allocationsApi, type UnitReportSummary } from './allocations' +export { allocationsApi, changeRequestsApi, type UnitReportSummary, type ConsumptionReport, type ChangeRequest } from './allocations' export { materialCategoriesApi } from './materialCategories' export { reportsApi } from './reports' export { personnelApi } from './personnel' diff --git a/src/frontend/src/api/personnel.ts b/src/frontend/src/api/personnel.ts index 11e8f59..ffba0c9 100644 --- a/src/frontend/src/api/personnel.ts +++ b/src/frontend/src/api/personnel.ts @@ -81,5 +81,27 @@ export const personnelApi = { async directUpgrade(personnelId: number): Promise { const response = await apiClient.post(`/personnel/${personnelId}/direct-upgrade`) return response.data + }, + + // 获取审批历史 + async getApprovalHistory(personnelId: number): Promise { + const response = await apiClient.get(`/personnel/${personnelId}/approval-history`) + return response.data } } + +// 审批历史类型 +export interface PersonnelApprovalHistory { + id: number + personnelId: number + action: string + previousStatus: string | null + newStatus: string + previousLevel: string | null + newLevel: string | null + reviewedByUserId: number + reviewedByUnitId: number | null + reviewedByUnitName?: string + comments: string | null + reviewedAt: string +} diff --git a/src/frontend/src/layouts/MainLayout.vue b/src/frontend/src/layouts/MainLayout.vue index 1367444..5ce1510 100644 --- a/src/frontend/src/layouts/MainLayout.vue +++ b/src/frontend/src/layouts/MainLayout.vue @@ -29,6 +29,7 @@ 配额列表 创建配额 + 删改申请 diff --git a/src/frontend/src/router/index.ts b/src/frontend/src/router/index.ts index ad73abd..00b93b9 100644 --- a/src/frontend/src/router/index.ts +++ b/src/frontend/src/router/index.ts @@ -59,6 +59,12 @@ const routes: RouteRecordRaw[] = [ component: () => import('@/views/allocations/AllocationReport.vue'), meta: { title: '上报消耗' } }, + { + path: 'allocations/change-requests', + name: 'ChangeRequests', + component: () => import('@/views/allocations/ChangeRequestList.vue'), + meta: { title: '删改申请' } + }, { path: 'reports', name: 'Reports', diff --git a/src/frontend/src/stores/auth.ts b/src/frontend/src/stores/auth.ts index 65838b2..1407067 100644 --- a/src/frontend/src/stores/auth.ts +++ b/src/frontend/src/stores/auth.ts @@ -95,6 +95,7 @@ export const useAuthStore = defineStore('auth', () => { isAuthenticated, organizationalLevel: computed(() => user.value?.organizationalLevel), organizationalLevelNum, + unitId: computed(() => user.value?.organizationalUnitId), canManageSubordinates, canCreateAllocations, canApprove, diff --git a/src/frontend/src/views/AuditLogs.vue b/src/frontend/src/views/AuditLogs.vue index 2d32d9d..7b88f8f 100644 --- a/src/frontend/src/views/AuditLogs.vue +++ b/src/frontend/src/views/AuditLogs.vue @@ -123,17 +123,6 @@ {{ selectedLog.description || '-' }} - {{ selectedLog.requestPath || '-' }} - {{ selectedLog.userAgent || '-' }} - -
{{ formatJson(selectedLog.oldValues) }}
-
- -
{{ formatJson(selectedLog.newValues) }}
-
- -
{{ formatJson(selectedLog.changedFields) }}
-
{{ selectedLog.errorMessage }} diff --git a/src/frontend/src/views/allocations/AllocationList.vue b/src/frontend/src/views/allocations/AllocationList.vue index 6fb69da..9cf2da0 100644 --- a/src/frontend/src/views/allocations/AllocationList.vue +++ b/src/frontend/src/views/allocations/AllocationList.vue @@ -596,6 +596,8 @@ function getTotalConsumed(): number { } function canReportConsumption(distribution: AllocationDistribution): boolean { + // 师级(Division)是配额创建者,不需要上报消耗 + if (authStore.organizationalLevelNum === 1) return false // 当前用户所属单位或其上级单位的分配都可以上报消耗 if (!authStore.user) return false // 营部及以下账号可以上报所属团的配额 diff --git a/src/frontend/src/views/allocations/AllocationReport.vue b/src/frontend/src/views/allocations/AllocationReport.vue index 6d34819..9dbdcca 100644 --- a/src/frontend/src/views/allocations/AllocationReport.vue +++ b/src/frontend/src/views/allocations/AllocationReport.vue @@ -188,9 +188,84 @@ {{ row.remarks || '-' }} + + + + + + +
+ + + {{ formatDate(selectedReport.reportedAt) }} + + + {{ formatNumber(selectedReport.reportedAmount) }} {{ allocation?.unit }} + + + {{ selectedReport.remarks || '-' }} + + +
+ + + + + 删除 + 申请删除此条记录 + + + 修改 + 申请修改此条记录 + + + + + + + + +
@@ -203,10 +278,15 @@ import { ref, reactive, computed, onMounted } from 'vue' import { useRouter, useRoute } from 'vue-router' import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus' import { DocumentAdd, Back, Check, Close, Clock } from '@element-plus/icons-vue' -import { allocationsApi, type ConsumptionReport } from '@/api' +import { allocationsApi, changeRequestsApi, type ConsumptionReport } from '@/api' import { useAuthStore } from '@/stores/auth' import type { MaterialAllocation, AllocationDistribution } from '@/types' +interface ConsumptionReportWithPending extends ConsumptionReport { + hasPendingRequest?: boolean + reportedByUnitId?: number +} + const router = useRouter() const route = useRoute() const authStore = useAuthStore() @@ -215,9 +295,29 @@ const loading = ref(false) const submitting = ref(false) const allocation = ref(null) const distribution = ref(null) -const consumptionReports = ref([]) +const consumptionReports = ref([]) const formRef = ref() +// 申请删改相关 +const changeRequestDialogVisible = ref(false) +const changeRequestSubmitting = ref(false) +const selectedReport = ref(null) +const changeRequestFormRef = ref() +const changeRequestForm = reactive({ + requestType: 'Delete' as 'Delete' | 'Modify', + reason: '' +}) + +const changeRequestRules: FormRules = { + requestType: [ + { required: true, message: '请选择申请类型', trigger: 'change' } + ], + reason: [ + { required: true, message: '请输入申请原因', trigger: 'blur' }, + { min: 5, message: '申请原因至少5个字符', trigger: 'blur' } + ] +} + const form = reactive({ actualCompletion: 0, remarks: '', @@ -431,12 +531,70 @@ async function loadData() { async function loadConsumptionReports(distributionId: number) { try { - consumptionReports.value = await allocationsApi.getConsumptionReports(distributionId) + const reports = await allocationsApi.getConsumptionReports(distributionId) + // 检查每条记录是否有待处理的申请 + consumptionReports.value = await Promise.all( + reports.map(async (report) => { + try { + const { hasPendingRequest } = await changeRequestsApi.checkPending(report.id) + return { ...report, hasPendingRequest } + } catch { + return { ...report, hasPendingRequest: false } + } + }) + ) } catch { console.error('加载上报历史失败') } } +// 判断是否可以申请删改(只有本单位的记录才能申请) +function canRequestChange(report: ConsumptionReportWithPending): boolean { + // 如果已有待处理的申请,不能再申请 + if (report.hasPendingRequest) return false + // 只有本单位的记录才能申请删改 + // 通过 reportedByUnitId 判断,如果没有这个字段,则通过 reportedByUserName 判断 + // 这里简化处理:如果记录的上报单位是当前用户的单位,则可以申请 + return report.reportedByUnitId === authStore.unitId +} + +// 打开申请删改对话框 +function openChangeRequestDialog(report: ConsumptionReportWithPending) { + selectedReport.value = report + changeRequestForm.requestType = 'Delete' + changeRequestForm.reason = '' + changeRequestDialogVisible.value = true +} + +// 提交申请删改 +async function submitChangeRequest() { + if (!changeRequestFormRef.value || !selectedReport.value) return + + try { + await changeRequestFormRef.value.validate() + + changeRequestSubmitting.value = true + + await changeRequestsApi.create({ + consumptionReportId: selectedReport.value.id, + requestType: changeRequestForm.requestType, + reason: changeRequestForm.reason + }) + + ElMessage.success('申请已提交,等待上级审批') + changeRequestDialogVisible.value = false + + // 刷新上报历史 + if (distribution.value) { + await loadConsumptionReports(distribution.value.id) + } + } catch (error: any) { + ElMessage.error(error.response?.data?.message || '提交申请失败') + } finally { + changeRequestSubmitting.value = false + } +} + onMounted(() => { loadData() }) @@ -629,16 +787,25 @@ onMounted(() => { font-size: 13px; } -:deep(.el-radio-group) { +.change-request-radio-group { display: flex; flex-direction: column; gap: 12px; } -:deep(.el-radio) { +.change-request-radio-group :deep(.el-radio) { display: flex; align-items: center; height: auto; padding: 8px 0; + margin-right: 0; +} + +.no-action { + color: #c0c4cc; +} + +.change-request-info { + margin-bottom: 16px; } diff --git a/src/frontend/src/views/allocations/ChangeRequestList.vue b/src/frontend/src/views/allocations/ChangeRequestList.vue new file mode 100644 index 0000000..4027293 --- /dev/null +++ b/src/frontend/src/views/allocations/ChangeRequestList.vue @@ -0,0 +1,369 @@ + + + + + diff --git a/src/frontend/src/views/personnel/PersonnelDetail.vue b/src/frontend/src/views/personnel/PersonnelDetail.vue index a1c647d..533ac13 100644 --- a/src/frontend/src/views/personnel/PersonnelDetail.vue +++ b/src/frontend/src/views/personnel/PersonnelDetail.vue @@ -19,7 +19,7 @@
- {{ getStatusName(person.status) }} + {{ person.pendingUpgradeByUnitId ? '待上级审批' : getStatusName(person.status) }}
@@ -71,6 +71,36 @@ + + + 审批历史 + + + +
+
+ {{ getActionName(item.action) }} + {{ item.reviewedByUnitName }} +
+
+ {{ getLevelName(item.previousLevel) }} → + {{ getLevelName(item.newLevel) }} +
+
+ + {{ item.comments }} +
+
+
+
+
+ @@ -80,14 +110,16 @@ import { ref, onMounted } from 'vue' import { useRoute } from 'vue-router' import { ElMessage } from 'element-plus' -import { User, Document } from '@element-plus/icons-vue' +import { User, Document, ChatDotRound } from '@element-plus/icons-vue' import { personnelApi } from '@/api' +import type { PersonnelApprovalHistory } from '@/api/personnel' import type { Personnel } from '@/types' import { PersonnelStatus, PersonnelLevel } from '@/types' const route = useRoute() const person = ref(null) +const approvalHistory = ref([]) const loading = ref(false) function getStatusName(status: PersonnelStatus): string { @@ -108,22 +140,48 @@ function getStatusTagType(status: PersonnelStatus): string { } } -function getLevelName(level: PersonnelLevel): string { +function getLevelName(level: PersonnelLevel | string | null): string { switch (level) { - case PersonnelLevel.Division: return '师级人才' - case PersonnelLevel.Regiment: return '团级人才' - case PersonnelLevel.Battalion: return '营级人才' - case PersonnelLevel.Company: return '连级人才' + case PersonnelLevel.Division: + case 'Division': return '师级人才' + case PersonnelLevel.Regiment: + case 'Regiment': return '团级人才' + case PersonnelLevel.Battalion: + case 'Battalion': return '营级人才' + case PersonnelLevel.Company: + case 'Company': return '连级人才' default: return '' } } -function getLevelTagType(level: PersonnelLevel): string { +function getLevelTagType(level: PersonnelLevel | string | null): string { switch (level) { - case PersonnelLevel.Division: return 'danger' - case PersonnelLevel.Regiment: return 'warning' - case PersonnelLevel.Battalion: return 'success' - case PersonnelLevel.Company: return 'info' + case PersonnelLevel.Division: + case 'Division': return 'danger' + case PersonnelLevel.Regiment: + case 'Regiment': return 'warning' + case PersonnelLevel.Battalion: + case 'Battalion': return 'success' + case PersonnelLevel.Company: + case 'Company': return 'info' + default: return 'info' + } +} + +function getActionName(action: string): string { + switch (action) { + case 'Approved': return '审批通过' + case 'Rejected': return '审批拒绝' + case 'LevelUpgraded': return '等级升级' + default: return action + } +} + +function getActionType(action: string): string { + switch (action) { + case 'Approved': return 'success' + case 'Rejected': return 'danger' + case 'LevelUpgraded': return 'primary' default: return 'info' } } @@ -135,7 +193,16 @@ function formatDate(dateStr: string): string { async function loadPersonnel() { loading.value = true try { - person.value = await personnelApi.getById(Number(route.params.id)) + const id = Number(route.params.id) + person.value = await personnelApi.getById(id) + + // 加载审批历史 + try { + approvalHistory.value = await personnelApi.getApprovalHistory(id) + } catch { + // 审批历史加载失败不影响主要信息显示 + approvalHistory.value = [] + } } catch { ElMessage.error('加载人员信息失败') } finally { @@ -198,6 +265,56 @@ onMounted(() => { color: #303133; } +/* 审批历史样式 */ +.history-card { + padding: 0; +} + +.history-card :deep(.el-card__body) { + padding: 12px 16px; +} + +.history-content { + display: flex; + flex-direction: column; + gap: 8px; +} + +.history-action { + display: flex; + align-items: center; + gap: 8px; +} + +.history-unit { + color: #606266; + font-size: 13px; +} + +.history-level { + display: flex; + align-items: center; + gap: 4px; + font-size: 13px; + color: #909399; +} + +.history-comments { + display: flex; + align-items: flex-start; + gap: 6px; + padding: 8px 12px; + background-color: #f5f7fa; + border-radius: 4px; + font-size: 13px; + color: #606266; +} + +.history-comments .el-icon { + margin-top: 2px; + color: #909399; +} + /* 缩短标签列宽度 */ :deep(.el-descriptions__label) { width: 120px !important; diff --git a/src/frontend/src/views/personnel/PersonnelList.vue b/src/frontend/src/views/personnel/PersonnelList.vue index 5706a0c..8398b3f 100644 --- a/src/frontend/src/views/personnel/PersonnelList.vue +++ b/src/frontend/src/views/personnel/PersonnelList.vue @@ -20,10 +20,13 @@ - + 待审批 + + 待上级审批 + 已批准 @@ -314,13 +317,24 @@ function handleSearch() { async function loadPersonnel() { loading.value = true try { + // 处理状态筛选:PendingUpgrade 是前端特殊状态,需要单独处理 + const isPendingUpgradeFilter = statusFilter.value === 'PendingUpgrade' + const backendStatus = isPendingUpgradeFilter ? undefined : (statusFilter.value || undefined) + const response = await personnelApi.getAll({ pageNumber: pagination.pageNumber, pageSize: pagination.pageSize, - status: statusFilter.value || undefined + status: backendStatus }) - // 前端搜索过滤 + let items = response.items + + // 前端过滤:待上级审批(有 pendingUpgradeByUnitId 的记录) + if (isPendingUpgradeFilter) { + items = items.filter(p => p.pendingUpgradeByUnitId) + } + + // 前端搜索过滤 if (searchKeyword.value) { const keyword = searchKeyword.value.toLowerCase() items = items.filter(p => @@ -329,7 +343,7 @@ async function loadPersonnel() { ) } personnel.value = items - pagination.total = response.totalCount + pagination.total = isPendingUpgradeFilter ? items.length : response.totalCount } catch { ElMessage.error('加载人才列表失败') } finally {