修改上报

This commit is contained in:
18631081161 2026-01-17 02:35:54 +08:00
parent 2495d39e9e
commit 425ac307bb
27 changed files with 2546 additions and 51 deletions

View File

@ -428,6 +428,7 @@ public class AllocationsController : BaseApiController
cumulativeAmount = r.CumulativeAmount,
remarks = r.Remarks,
reportedByUserName = r.ReportedByUser?.DisplayName,
reportedByUnitId = r.ReportedByUnitId,
reportedAt = r.ReportedAt
});

View File

@ -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;
/// <summary>
/// 消耗记录删改申请控制器
/// </summary>
[Authorize]
public class ConsumptionChangeRequestsController : BaseApiController
{
private readonly ApplicationDbContext _context;
private readonly ILogger<ConsumptionChangeRequestsController> _logger;
public ConsumptionChangeRequestsController(
ApplicationDbContext context,
ILogger<ConsumptionChangeRequestsController> logger)
{
_context = context;
_logger = logger;
}
/// <summary>
/// 创建删改申请
/// </summary>
[HttpPost]
public async Task<IActionResult> 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 });
}
/// <summary>
/// 获取待处理的申请列表(上级单位查看下级的申请)
/// </summary>
[HttpGet("pending")]
public async Task<IActionResult> 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);
}
/// <summary>
/// 获取本单位的申请列表
/// </summary>
[HttpGet("my")]
public async Task<IActionResult> 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);
}
/// <summary>
/// 处理申请(同意或拒绝)
/// </summary>
[HttpPost("{id}/process")]
public async Task<IActionResult> 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}该申请" });
}
/// <summary>
/// 修改消耗记录(上级单位在同意修改申请后执行)
/// </summary>
[HttpPost("{id}/modify")]
public async Task<IActionResult> 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 = "修改成功" });
}
/// <summary>
/// 检查消耗记录是否有待处理的申请
/// </summary>
[HttpGet("check/{consumptionReportId}")]
public async Task<IActionResult> 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<List<int>> GetSubordinateUnitIds(int unitId)
{
var result = new List<int>();
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<bool> 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; }
}

View File

@ -551,7 +551,8 @@ public class PersonnelController : BaseApiController
public async Task<IActionResult> 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));
}
/// <summary>
@ -604,7 +605,8 @@ public class PersonnelController : BaseApiController
public async Task<IActionResult> 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);
}
/// <summary>

View File

@ -23,6 +23,7 @@ public class ApplicationDbContext : DbContext
public DbSet<ApprovalRequest> ApprovalRequests => Set<ApprovalRequest>();
public DbSet<AuditLog> AuditLogs => Set<AuditLog>();
public DbSet<ConsumptionReport> ConsumptionReports => Set<ConsumptionReport>();
public DbSet<ConsumptionReportChangeRequest> ConsumptionReportChangeRequests => Set<ConsumptionReportChangeRequest>();
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<ConsumptionReportChangeRequest>(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);
});
}
}

View File

@ -0,0 +1,874 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<decimal?>("ActualCompletion")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<int>("AllocationId")
.HasColumnType("int");
b.Property<DateTime?>("ApprovedAt")
.HasColumnType("datetime2");
b.Property<int?>("ApprovedByUserId")
.HasColumnType("int");
b.Property<bool>("IsApproved")
.HasColumnType("bit");
b.Property<DateTime?>("ReportedAt")
.HasColumnType("datetime2");
b.Property<int?>("ReportedByUserId")
.HasColumnType("int");
b.Property<int>("TargetUnitId")
.HasColumnType("int");
b.Property<decimal>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("OriginalData")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Reason")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<DateTime>("RequestedAt")
.HasColumnType("datetime2");
b.Property<int>("RequestedByUnitId")
.HasColumnType("int");
b.Property<int>("RequestedByUserId")
.HasColumnType("int");
b.Property<string>("RequestedChanges")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("ReviewComments")
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("ReviewedAt")
.HasColumnType("datetime2");
b.Property<int?>("ReviewedByUserId")
.HasColumnType("int");
b.Property<int>("Status")
.HasColumnType("int");
b.Property<int>("TargetEntityId")
.HasColumnType("int");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Action")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("ChangedFields")
.HasColumnType("nvarchar(max)");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<int>("EntityId")
.HasColumnType("int");
b.Property<string>("EntityType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("ErrorMessage")
.HasMaxLength(2000)
.HasColumnType("nvarchar(2000)");
b.Property<string>("IpAddress")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<bool>("IsSuccess")
.HasColumnType("bit");
b.Property<string>("NewValues")
.HasColumnType("nvarchar(max)");
b.Property<string>("OldValues")
.HasColumnType("nvarchar(max)");
b.Property<int?>("OrganizationalUnitId")
.HasColumnType("int");
b.Property<string>("RequestPath")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<DateTime>("Timestamp")
.HasColumnType("datetime2");
b.Property<string>("UserAgent")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<int?>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("AllocationDistributionId")
.HasColumnType("int");
b.Property<decimal>("CumulativeAmount")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<string>("Remarks")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<decimal>("ReportedAmount")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<DateTime>("ReportedAt")
.HasColumnType("datetime2");
b.Property<int>("ReportedByUnitId")
.HasColumnType("int");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("ConsumptionReportId")
.HasColumnType("int");
b.Property<string>("ProcessComments")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<DateTime?>("ProcessedAt")
.HasColumnType("datetime2");
b.Property<int?>("ProcessedByUnitId")
.HasColumnType("int");
b.Property<int?>("ProcessedByUserId")
.HasColumnType("int");
b.Property<string>("Reason")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<int>("RequestType")
.HasColumnType("int");
b.Property<DateTime>("RequestedAt")
.HasColumnType("datetime2");
b.Property<int>("RequestedByUnitId")
.HasColumnType("int");
b.Property<int>("RequestedByUserId")
.HasColumnType("int");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Category")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<int>("CreatedByUnitId")
.HasColumnType("int");
b.Property<string>("MaterialName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<decimal>("TotalQuota")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<string>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("MaterialCategories");
});
modelBuilder.Entity("MilitaryTrainingManagement.Models.Entities.OrganizationalUnit", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<int>("Level")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int?>("ParentId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("ParentId");
b.ToTable("OrganizationalUnits");
});
modelBuilder.Entity("MilitaryTrainingManagement.Models.Entities.Personnel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Achievements")
.HasColumnType("nvarchar(max)");
b.Property<int>("Age")
.HasColumnType("int");
b.Property<DateTime?>("ApprovedAt")
.HasColumnType("datetime2");
b.Property<int?>("ApprovedByUnitId")
.HasColumnType("int");
b.Property<int?>("ApprovedLevel")
.HasColumnType("int");
b.Property<string>("BirthDate")
.HasColumnType("nvarchar(max)");
b.Property<string>("ContactInfo")
.HasColumnType("nvarchar(max)");
b.Property<string>("EducationLevel")
.HasColumnType("nvarchar(max)");
b.Property<string>("EnlistmentDate")
.HasColumnType("nvarchar(max)");
b.Property<string>("Ethnicity")
.HasColumnType("nvarchar(max)");
b.Property<string>("Gender")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("nvarchar(10)");
b.Property<decimal?>("Height")
.HasPrecision(5, 2)
.HasColumnType("decimal(5,2)");
b.Property<string>("Hometown")
.HasColumnType("nvarchar(max)");
b.Property<string>("IdNumber")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<int?>("PendingUpgradeByUnitId")
.HasColumnType("int");
b.Property<string>("PhotoPath")
.HasColumnType("nvarchar(max)");
b.Property<string>("PoliticalStatus")
.HasColumnType("nvarchar(max)");
b.Property<string>("Position")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("ProfessionalTitle")
.HasColumnType("nvarchar(max)");
b.Property<string>("Rank")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Specialty")
.HasColumnType("nvarchar(max)");
b.Property<int>("Status")
.HasColumnType("int");
b.Property<DateTime>("SubmittedAt")
.HasColumnType("datetime2");
b.Property<int>("SubmittedByUnitId")
.HasColumnType("int");
b.Property<string>("SupportingDocuments")
.HasColumnType("nvarchar(max)");
b.Property<string>("TrainingParticipation")
.HasColumnType("nvarchar(max)");
b.Property<string>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("Action")
.HasColumnType("int");
b.Property<string>("Comments")
.HasColumnType("nvarchar(max)");
b.Property<int?>("NewLevel")
.HasColumnType("int");
b.Property<int>("NewStatus")
.HasColumnType("int");
b.Property<int>("PersonnelId")
.HasColumnType("int");
b.Property<int?>("PreviousLevel")
.HasColumnType("int");
b.Property<int?>("PreviousStatus")
.HasColumnType("int");
b.Property<DateTime>("ReviewedAt")
.HasColumnType("datetime2");
b.Property<int?>("ReviewedByUnitId")
.HasColumnType("int");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<DateTime?>("LastLoginAt")
.HasColumnType("datetime2");
b.Property<int>("OrganizationalUnitId")
.HasColumnType("int");
b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("PlainPassword")
.HasColumnType("nvarchar(max)");
b.Property<string>("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
}
}
}

View File

@ -0,0 +1,162 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace MilitaryTrainingManagement.Migrations
{
/// <inheritdoc />
public partial class AddConsumptionReportChangeRequests : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "PendingUpgradeByUnitId",
table: "Personnel",
type: "int",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "ReportedByUnitId",
table: "ConsumptionReports",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateTable(
name: "ConsumptionReportChangeRequests",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
ConsumptionReportId = table.Column<int>(type: "int", nullable: false),
RequestType = table.Column<int>(type: "int", nullable: false),
Reason = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
Status = table.Column<int>(type: "int", nullable: false),
RequestedByUnitId = table.Column<int>(type: "int", nullable: false),
RequestedByUserId = table.Column<int>(type: "int", nullable: false),
RequestedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
ProcessedByUnitId = table.Column<int>(type: "int", nullable: true),
ProcessedByUserId = table.Column<int>(type: "int", nullable: true),
ProcessedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
ProcessComments = table.Column<string>(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");
}
/// <inheritdoc />
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");
}
}
}

View File

@ -236,6 +236,9 @@ namespace MilitaryTrainingManagement.Migrations
b.Property<DateTime>("ReportedAt")
.HasColumnType("datetime2");
b.Property<int>("ReportedByUnitId")
.HasColumnType("int");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("ConsumptionReportId")
.HasColumnType("int");
b.Property<string>("ProcessComments")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<DateTime?>("ProcessedAt")
.HasColumnType("datetime2");
b.Property<int?>("ProcessedByUnitId")
.HasColumnType("int");
b.Property<int?>("ProcessedByUserId")
.HasColumnType("int");
b.Property<string>("Reason")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<int>("RequestType")
.HasColumnType("int");
b.Property<DateTime>("RequestedAt")
.HasColumnType("datetime2");
b.Property<int>("RequestedByUnitId")
.HasColumnType("int");
b.Property<int>("RequestedByUserId")
.HasColumnType("int");
b.Property<int>("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<int>("Id")
@ -412,6 +478,9 @@ namespace MilitaryTrainingManagement.Migrations
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<int?>("PendingUpgradeByUnitId")
.HasColumnType("int");
b.Property<string>("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");
});

View File

@ -23,6 +23,12 @@ public class ConsumptionReport
[ForeignKey(nameof(AllocationDistributionId))]
public AllocationDistribution AllocationDistribution { get; set; } = null!;
/// <summary>
/// 配额分配(别名,用于兼容)
/// </summary>
[NotMapped]
public AllocationDistribution Distribution => AllocationDistribution;
/// <summary>
/// 本次上报数量
/// </summary>
@ -55,6 +61,18 @@ public class ConsumptionReport
[ForeignKey(nameof(ReportedByUserId))]
public UserAccount ReportedByUser { get; set; } = null!;
/// <summary>
/// 上报单位ID
/// </summary>
[Required]
public int ReportedByUnitId { get; set; }
/// <summary>
/// 上报单位
/// </summary>
[ForeignKey(nameof(ReportedByUnitId))]
public OrganizationalUnit ReportedByUnit { get; set; } = null!;
/// <summary>
/// 上报时间
/// </summary>

View File

@ -0,0 +1,91 @@
using MilitaryTrainingManagement.Models.Enums;
namespace MilitaryTrainingManagement.Models.Entities;
/// <summary>
/// 消耗记录删改申请
/// </summary>
public class ConsumptionReportChangeRequest
{
public int Id { get; set; }
/// <summary>
/// 关联的消耗记录ID
/// </summary>
public int ConsumptionReportId { get; set; }
/// <summary>
/// 关联的消耗记录
/// </summary>
public ConsumptionReport ConsumptionReport { get; set; } = null!;
/// <summary>
/// 申请类型Delete删除或 Modify修改
/// </summary>
public ChangeRequestType RequestType { get; set; }
/// <summary>
/// 申请原因
/// </summary>
public string Reason { get; set; } = string.Empty;
/// <summary>
/// 申请状态
/// </summary>
public ChangeRequestStatus Status { get; set; } = ChangeRequestStatus.Pending;
/// <summary>
/// 申请单位ID
/// </summary>
public int RequestedByUnitId { get; set; }
/// <summary>
/// 申请单位
/// </summary>
public OrganizationalUnit RequestedByUnit { get; set; } = null!;
/// <summary>
/// 申请人用户ID
/// </summary>
public int RequestedByUserId { get; set; }
/// <summary>
/// 申请人
/// </summary>
public UserAccount RequestedByUser { get; set; } = null!;
/// <summary>
/// 申请时间
/// </summary>
public DateTime RequestedAt { get; set; } = DateTime.UtcNow;
/// <summary>
/// 处理单位ID
/// </summary>
public int? ProcessedByUnitId { get; set; }
/// <summary>
/// 处理单位
/// </summary>
public OrganizationalUnit? ProcessedByUnit { get; set; }
/// <summary>
/// 处理人用户ID
/// </summary>
public int? ProcessedByUserId { get; set; }
/// <summary>
/// 处理人
/// </summary>
public UserAccount? ProcessedByUser { get; set; }
/// <summary>
/// 处理时间
/// </summary>
public DateTime? ProcessedAt { get; set; }
/// <summary>
/// 处理意见
/// </summary>
public string? ProcessComments { get; set; }
}

View File

@ -0,0 +1,14 @@
namespace MilitaryTrainingManagement.Models.Enums;
/// <summary>
/// 删改申请状态
/// </summary>
public enum ChangeRequestStatus
{
/// <summary>待处理</summary>
Pending = 1,
/// <summary>已同意</summary>
Approved = 2,
/// <summary>已拒绝</summary>
Rejected = 3
}

View File

@ -0,0 +1,12 @@
namespace MilitaryTrainingManagement.Models.Enums;
/// <summary>
/// 删改申请类型
/// </summary>
public enum ChangeRequestType
{
/// <summary>删除</summary>
Delete = 1,
/// <summary>修改</summary>
Modify = 2
}

View File

@ -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())
{

View File

@ -316,6 +316,7 @@ public class AllocationService : IAllocationService
CumulativeAmount = actualCompletion,
Remarks = remarks,
ReportedByUserId = userId,
ReportedByUnitId = unitId,
ReportedAt = DateTime.UtcNow
};
_context.ConsumptionReports.Add(consumptionReport);

View File

@ -179,7 +179,7 @@ public class PersonnelService : IPersonnelService
};
}
public async Task<Personnel> RejectAsync(int personnelId, int reviewedByUserId)
public async Task<Personnel> 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<IEnumerable<Personnel>> BatchRejectPersonnelAsync(int[] personnelIds, int reviewedByUserId, string reason)
public async Task<IEnumerable<Personnel>> BatchRejectPersonnelAsync(int[] personnelIds, int reviewedByUserId, int reviewedByUnitId, string reason)
{
var rejectedPersonnel = new List<Personnel>();
@ -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)
{

View File

@ -13,7 +13,7 @@ public interface IPersonnelService
Task<Personnel> CreateAsync(Personnel personnel);
Task<Personnel> UpdateAsync(Personnel personnel);
Task<Personnel> ApproveAsync(int personnelId, int approvedByUnitId, PersonnelLevel? level = null);
Task<Personnel> RejectAsync(int personnelId, int reviewedByUserId);
Task<Personnel> RejectAsync(int personnelId, int reviewedByUserId, int reviewedByUnitId, string? comments = null);
Task DeleteAsync(int id);
/// <summary>
@ -64,7 +64,7 @@ public interface IPersonnelService
/// <summary>
/// 批量拒绝人员
/// </summary>
Task<IEnumerable<Personnel>> BatchRejectPersonnelAsync(int[] personnelIds, int reviewedByUserId, string reason);
Task<IEnumerable<Personnel>> BatchRejectPersonnelAsync(int[] personnelIds, int reviewedByUserId, int reviewedByUnitId, string reason);
/// <summary>
/// 获取人员审批历史

View File

@ -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<ChangeRequest[]> {
const response = await apiClient.get<ChangeRequest[]>('/ConsumptionChangeRequests/pending')
return response.data
},
// 获取本单位的申请
async getMy(): Promise<ChangeRequest[]> {
const response = await apiClient.get<ChangeRequest[]>('/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
}
}

View File

@ -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'

View File

@ -81,5 +81,27 @@ export const personnelApi = {
async directUpgrade(personnelId: number): Promise<Personnel> {
const response = await apiClient.post<Personnel>(`/personnel/${personnelId}/direct-upgrade`)
return response.data
},
// 获取审批历史
async getApprovalHistory(personnelId: number): Promise<PersonnelApprovalHistory[]> {
const response = await apiClient.get<PersonnelApprovalHistory[]>(`/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
}

View File

@ -29,6 +29,7 @@
</template>
<el-menu-item index="/allocations">配额列表</el-menu-item>
<el-menu-item v-if="authStore.canCreateAllocations" index="/allocations/create">创建配额</el-menu-item>
<el-menu-item index="/allocations/change-requests">删改申请</el-menu-item>
</el-sub-menu>
<el-sub-menu index="personnel">

View File

@ -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',

View File

@ -95,6 +95,7 @@ export const useAuthStore = defineStore('auth', () => {
isAuthenticated,
organizationalLevel: computed(() => user.value?.organizationalLevel),
organizationalLevelNum,
unitId: computed(() => user.value?.organizationalUnitId),
canManageSubordinates,
canCreateAllocations,
canApprove,

View File

@ -123,17 +123,6 @@
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="描述" :span="2">{{ selectedLog.description || '-' }}</el-descriptions-item>
<el-descriptions-item label="请求路径" :span="2">{{ selectedLog.requestPath || '-' }}</el-descriptions-item>
<el-descriptions-item label="User Agent" :span="2">{{ selectedLog.userAgent || '-' }}</el-descriptions-item>
<el-descriptions-item v-if="selectedLog.oldValues" label="原始值" :span="2">
<pre class="json-content">{{ formatJson(selectedLog.oldValues) }}</pre>
</el-descriptions-item>
<el-descriptions-item v-if="selectedLog.newValues" label="新值" :span="2">
<pre class="json-content">{{ formatJson(selectedLog.newValues) }}</pre>
</el-descriptions-item>
<el-descriptions-item v-if="selectedLog.changedFields" label="变更字段" :span="2">
<pre class="json-content">{{ formatJson(selectedLog.changedFields) }}</pre>
</el-descriptions-item>
<el-descriptions-item v-if="selectedLog.errorMessage" label="错误信息" :span="2">
<el-text type="danger">{{ selectedLog.errorMessage }}</el-text>
</el-descriptions-item>

View File

@ -596,6 +596,8 @@ function getTotalConsumed(): number {
}
function canReportConsumption(distribution: AllocationDistribution): boolean {
// Division
if (authStore.organizationalLevelNum === 1) return false
//
if (!authStore.user) return false
//

View File

@ -188,9 +188,84 @@
<span class="remarks-text">{{ row.remarks || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center">
<template #default="{ row }">
<el-button
v-if="canRequestChange(row)"
type="warning"
size="small"
link
@click="openChangeRequestDialog(row)"
>
申请删改
</el-button>
<el-tag v-else-if="row.hasPendingRequest" type="info" size="small">
申请中
</el-tag>
<span v-else class="no-action">-</span>
</template>
</el-table-column>
</el-table>
<el-empty v-else description="暂无上报记录" :image-size="80" />
</div>
<!-- 申请删改对话框 -->
<el-dialog
v-model="changeRequestDialogVisible"
title="申请删改"
width="500px"
:close-on-click-modal="false"
>
<div v-if="selectedReport" class="change-request-info">
<el-descriptions :column="1" border size="small">
<el-descriptions-item label="上报时间">
{{ formatDate(selectedReport.reportedAt) }}
</el-descriptions-item>
<el-descriptions-item label="上报数量">
{{ formatNumber(selectedReport.reportedAmount) }} {{ allocation?.unit }}
</el-descriptions-item>
<el-descriptions-item label="备注">
{{ selectedReport.remarks || '-' }}
</el-descriptions-item>
</el-descriptions>
</div>
<el-form
ref="changeRequestFormRef"
:model="changeRequestForm"
:rules="changeRequestRules"
label-width="100px"
style="margin-top: 20px"
>
<el-form-item label="申请类型" prop="requestType">
<el-radio-group v-model="changeRequestForm.requestType" class="change-request-radio-group">
<el-radio value="Delete">
<el-tag type="danger" size="small">删除</el-tag>
<span class="radio-desc">申请删除此条记录</span>
</el-radio>
<el-radio value="Modify">
<el-tag type="warning" size="small">修改</el-tag>
<span class="radio-desc">申请修改此条记录</span>
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="申请原因" prop="reason">
<el-input
v-model="changeRequestForm.reason"
type="textarea"
:rows="4"
placeholder="请输入申请原因"
maxlength="500"
show-word-limit
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="changeRequestDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="changeRequestSubmitting" @click="submitChangeRequest">
提交申请
</el-button>
</template>
</el-dialog>
</div>
<el-empty v-else description="未找到配额分配信息" />
@ -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<MaterialAllocation | null>(null)
const distribution = ref<AllocationDistribution | null>(null)
const consumptionReports = ref<ConsumptionReport[]>([])
const consumptionReports = ref<ConsumptionReportWithPending[]>([])
const formRef = ref<FormInstance>()
//
const changeRequestDialogVisible = ref(false)
const changeRequestSubmitting = ref(false)
const selectedReport = ref<ConsumptionReportWithPending | null>(null)
const changeRequestFormRef = ref<FormInstance>()
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;
}
</style>

View File

@ -0,0 +1,369 @@
<template>
<div class="change-request-list">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<div class="header-title">
<el-icon class="title-icon" :size="22"><Document /></el-icon>
<span>消耗记录删改申请</span>
</div>
<el-radio-group v-model="viewMode" size="small">
<el-radio-button value="pending">待处理</el-radio-button>
<el-radio-button value="my">我的申请</el-radio-button>
</el-radio-group>
</div>
</template>
<div v-if="loading" class="loading-container">
<el-skeleton :rows="6" animated />
</div>
<template v-else>
<!-- 待处理申请列表上级查看下级的 -->
<div v-if="viewMode === 'pending'">
<el-empty v-if="pendingRequests.length === 0" description="暂无待处理的申请" />
<el-table v-else :data="pendingRequests" stripe>
<el-table-column label="申请时间" width="170" align="center">
<template #default="{ row }">
{{ formatDate(row.requestedAt) }}
</template>
</el-table-column>
<el-table-column label="申请单位" width="120" align="center">
<template #default="{ row }">
{{ row.requestedByUnitName }}
</template>
</el-table-column>
<el-table-column label="申请类型" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.requestType === 'Delete' ? 'danger' : 'warning'" size="small">
{{ row.requestType === 'Delete' ? '删除' : '修改' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="物资名称" width="120">
<template #default="{ row }">
{{ row.consumptionReport?.materialName || '-' }}
</template>
</el-table-column>
<el-table-column label="上报数量" width="100" align="center">
<template #default="{ row }">
{{ row.consumptionReport?.reportedAmount }} {{ row.consumptionReport?.unit }}
</template>
</el-table-column>
<el-table-column label="申请原因" min-width="150">
<template #default="{ row }">
{{ row.reason }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" align="center" fixed="right">
<template #default="{ row }">
<el-button type="success" size="small" @click="handleApprove(row)">
同意
</el-button>
<el-button type="danger" size="small" @click="handleReject(row)">
拒绝
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 我的申请列表 -->
<div v-else>
<el-empty v-if="myRequests.length === 0" description="暂无申请记录" />
<el-table v-else :data="myRequests" stripe>
<el-table-column label="申请时间" width="170" align="center">
<template #default="{ row }">
{{ formatDate(row.requestedAt) }}
</template>
</el-table-column>
<el-table-column label="申请类型" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.requestType === 'Delete' ? 'danger' : 'warning'" size="small">
{{ row.requestType === 'Delete' ? '删除' : '修改' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="申请原因" min-width="150">
<template #default="{ row }">
{{ row.reason }}
</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="getStatusTagType(row.status)" size="small">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="处理时间" width="170" align="center">
<template #default="{ row }">
{{ row.processedAt ? formatDate(row.processedAt) : '-' }}
</template>
</el-table-column>
<el-table-column label="处理单位" width="120" align="center">
<template #default="{ row }">
{{ row.processedByUnitName || '-' }}
</template>
</el-table-column>
<el-table-column label="处理意见" min-width="150">
<template #default="{ row }">
{{ row.processComments || '-' }}
</template>
</el-table-column>
</el-table>
</div>
</template>
</el-card>
<!-- 拒绝原因对话框 -->
<el-dialog v-model="rejectDialogVisible" title="拒绝申请" width="400px">
<el-form :model="rejectForm" label-width="80px">
<el-form-item label="拒绝原因">
<el-input
v-model="rejectForm.comments"
type="textarea"
:rows="3"
placeholder="请输入拒绝原因(可选)"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="rejectDialogVisible = false">取消</el-button>
<el-button type="danger" :loading="processing" @click="confirmReject">
确认拒绝
</el-button>
</template>
</el-dialog>
<!-- 修改数据对话框 -->
<el-dialog v-model="modifyDialogVisible" title="修改消耗记录" width="500px">
<div v-if="selectedRequest?.consumptionReport" class="modify-info">
<el-descriptions :column="1" border size="small">
<el-descriptions-item label="物资名称">
{{ selectedRequest.consumptionReport.materialName }}
</el-descriptions-item>
<el-descriptions-item label="原上报数量">
{{ selectedRequest.consumptionReport.reportedAmount }} {{ selectedRequest.consumptionReport.unit }}
</el-descriptions-item>
<el-descriptions-item label="原备注">
{{ selectedRequest.consumptionReport.remarks || '-' }}
</el-descriptions-item>
</el-descriptions>
</div>
<el-form :model="modifyForm" label-width="100px" style="margin-top: 20px">
<el-form-item label="新数量" required>
<el-input-number
v-model="modifyForm.newAmount"
:min="0"
:precision="2"
style="width: 200px"
/>
<span style="margin-left: 8px">{{ selectedRequest?.consumptionReport?.unit }}</span>
</el-form-item>
<el-form-item label="新备注">
<el-input
v-model="modifyForm.newRemarks"
type="textarea"
:rows="2"
placeholder="请输入新备注(可选)"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="modifyDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="processing" @click="confirmModify">
确认修改
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Document } from '@element-plus/icons-vue'
import { changeRequestsApi, type ChangeRequest } from '@/api'
const loading = ref(false)
const processing = ref(false)
const viewMode = ref<'pending' | 'my'>('pending')
const pendingRequests = ref<ChangeRequest[]>([])
const myRequests = ref<ChangeRequest[]>([])
const rejectDialogVisible = ref(false)
const modifyDialogVisible = ref(false)
const selectedRequest = ref<ChangeRequest | null>(null)
const rejectForm = reactive({ comments: '' })
const modifyForm = reactive({ newAmount: 0, newRemarks: '' })
function formatDate(dateStr: string): string {
return new Date(dateStr).toLocaleString('zh-CN')
}
function getStatusTagType(status: string): 'success' | 'warning' | 'danger' | 'info' {
switch (status) {
case 'Approved': return 'success'
case 'Rejected': return 'danger'
case 'Pending': return 'warning'
default: return 'info'
}
}
function getStatusText(status: string): string {
switch (status) {
case 'Approved': return '已同意'
case 'Rejected': return '已拒绝'
case 'Pending': return '待处理'
default: return status
}
}
async function loadPendingRequests() {
try {
pendingRequests.value = await changeRequestsApi.getPending()
} catch (error: any) {
console.error('加载待处理申请失败', error)
}
}
async function loadMyRequests() {
try {
myRequests.value = await changeRequestsApi.getMy()
} catch (error: any) {
console.error('加载我的申请失败', error)
}
}
async function loadData() {
loading.value = true
try {
await Promise.all([loadPendingRequests(), loadMyRequests()])
} finally {
loading.value = false
}
}
async function handleApprove(request: ChangeRequest) {
if (request.requestType === 'Delete') {
//
try {
await ElMessageBox.confirm(
'同意后将自动删除该消耗记录,确定同意吗?',
'确认同意',
{ type: 'warning' }
)
processing.value = true
await changeRequestsApi.process(request.id, { approved: true })
ElMessage.success('已同意删除申请')
await loadData()
} catch (error: any) {
if (error !== 'cancel') {
ElMessage.error(error.response?.data?.message || '操作失败')
}
} finally {
processing.value = false
}
} else {
//
selectedRequest.value = request
modifyForm.newAmount = request.consumptionReport?.reportedAmount || 0
modifyForm.newRemarks = request.consumptionReport?.remarks || ''
modifyDialogVisible.value = true
}
}
function handleReject(request: ChangeRequest) {
selectedRequest.value = request
rejectForm.comments = ''
rejectDialogVisible.value = true
}
async function confirmReject() {
if (!selectedRequest.value) return
processing.value = true
try {
await changeRequestsApi.process(selectedRequest.value.id, {
approved: false,
comments: rejectForm.comments
})
ElMessage.success('已拒绝申请')
rejectDialogVisible.value = false
await loadData()
} catch (error: any) {
ElMessage.error(error.response?.data?.message || '操作失败')
} finally {
processing.value = false
}
}
async function confirmModify() {
if (!selectedRequest.value) return
if (modifyForm.newAmount <= 0) {
ElMessage.warning('新数量必须大于0')
return
}
processing.value = true
try {
//
await changeRequestsApi.process(selectedRequest.value.id, { approved: true })
//
await changeRequestsApi.modify(selectedRequest.value.id, {
newAmount: modifyForm.newAmount,
newRemarks: modifyForm.newRemarks
})
ElMessage.success('修改成功')
modifyDialogVisible.value = false
await loadData()
} catch (error: any) {
ElMessage.error(error.response?.data?.message || '操作失败')
} finally {
processing.value = false
}
}
watch(viewMode, () => {
//
})
onMounted(() => {
loadData()
})
</script>
<style scoped>
.change-request-list {
padding: 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-title {
display: flex;
align-items: center;
font-size: 18px;
font-weight: 600;
color: #303133;
}
.title-icon {
margin-right: 8px;
color: #409EFF;
}
.loading-container {
padding: 20px;
}
.modify-info {
margin-bottom: 16px;
}
</style>

View File

@ -19,7 +19,7 @@
</div>
<div class="status-badge">
<el-tag :type="getStatusTagType(person.status)" size="large">
{{ getStatusName(person.status) }}
{{ person.pendingUpgradeByUnitId ? '待上级审批' : getStatusName(person.status) }}
</el-tag>
</div>
</div>
@ -71,6 +71,36 @@
</div>
</el-col>
</el-row>
<!-- 审批历史 -->
<el-divider content-position="left">审批历史</el-divider>
<el-timeline v-if="approvalHistory.length > 0">
<el-timeline-item
v-for="item in approvalHistory"
:key="item.id"
:timestamp="formatDate(item.reviewedAt)"
:type="getActionType(item.action)"
placement="top"
>
<el-card shadow="hover" class="history-card">
<div class="history-content">
<div class="history-action">
<el-tag :type="getActionType(item.action)" size="small">{{ getActionName(item.action) }}</el-tag>
<span v-if="item.reviewedByUnitName" class="history-unit">{{ item.reviewedByUnitName }}</span>
</div>
<div v-if="item.newLevel" class="history-level">
<span v-if="item.previousLevel">{{ getLevelName(item.previousLevel) }} </span>
<el-tag :type="getLevelTagType(item.newLevel)" size="small">{{ getLevelName(item.newLevel) }}</el-tag>
</div>
<div v-if="item.comments" class="history-comments">
<el-icon><ChatDotRound /></el-icon>
<span>{{ item.comments }}</span>
</div>
</div>
</el-card>
</el-timeline-item>
</el-timeline>
<el-empty v-else description="暂无审批记录" :image-size="60" />
</div>
</el-card>
</div>
@ -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<Personnel | null>(null)
const approvalHistory = ref<PersonnelApprovalHistory[]>([])
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;

View File

@ -20,10 +20,13 @@
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-select v-model="statusFilter" placeholder="状态筛选" clearable style="width: 120px; margin-right: 12px">
<el-select v-model="statusFilter" placeholder="状态筛选" clearable style="width: 140px; margin-right: 12px">
<el-option label="待审批" value="Pending">
<el-tag type="warning" size="small">待审批</el-tag>
</el-option>
<el-option label="待上级审批" value="PendingUpgrade">
<el-tag type="primary" size="small">待上级审批</el-tag>
</el-option>
<el-option label="已批准" value="Approved">
<el-tag type="success" size="small">已批准</el-tag>
</el-option>
@ -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 {