diff --git a/src/MilitaryTrainingManagement/Authorization/IOrganizationalAuthorizationService.cs b/src/MilitaryTrainingManagement/Authorization/IOrganizationalAuthorizationService.cs index ec5aca8..8f44d3f 100644 --- a/src/MilitaryTrainingManagement/Authorization/IOrganizationalAuthorizationService.cs +++ b/src/MilitaryTrainingManagement/Authorization/IOrganizationalAuthorizationService.cs @@ -21,4 +21,9 @@ public interface IOrganizationalAuthorizationService /// 获取用户可访问的所有组织单位ID(包括自身和所有下级) /// Task> GetAccessibleUnitIdsAsync(int userUnitId); + + /// + /// 检查目标单位是否是当前单位的上级单位 + /// + Task IsAncestorUnitAsync(int targetUnitId, int currentUnitId); } diff --git a/src/MilitaryTrainingManagement/Authorization/OrganizationalAuthorizationService.cs b/src/MilitaryTrainingManagement/Authorization/OrganizationalAuthorizationService.cs index 87f1e5b..53cfadf 100644 --- a/src/MilitaryTrainingManagement/Authorization/OrganizationalAuthorizationService.cs +++ b/src/MilitaryTrainingManagement/Authorization/OrganizationalAuthorizationService.cs @@ -31,4 +31,11 @@ public class OrganizationalAuthorizationService : IOrganizationalAuthorizationSe var subordinateIds = await _organizationService.GetAllSubordinateIdsAsync(userUnitId); return new[] { userUnitId }.Concat(subordinateIds); } + + public async Task IsAncestorUnitAsync(int targetUnitId, int currentUnitId) + { + // 检查 targetUnitId 是否是 currentUnitId 的上级单位 + var ancestorIds = await _organizationService.GetAllAncestorIdsAsync(currentUnitId); + return ancestorIds.Contains(targetUnitId); + } } diff --git a/src/MilitaryTrainingManagement/Controllers/AllocationsController.cs b/src/MilitaryTrainingManagement/Controllers/AllocationsController.cs index 5a0cbc5..bb14426 100644 --- a/src/MilitaryTrainingManagement/Controllers/AllocationsController.cs +++ b/src/MilitaryTrainingManagement/Controllers/AllocationsController.cs @@ -110,10 +110,12 @@ public class AllocationsController : BaseApiController if (allocation == null) return NotFound(new { message = "配额不存在" }); - // 检查访问权限:只能查看自己创建的或分配给自己及下级的配额 + // 检查访问权限:可以查看自己创建的、分配给自己及下级的、或分配给上级单位的配额 var canAccess = allocation.CreatedByUnitId == unitId.Value || allocation.Distributions.Any(d => - _authorizationService.CanAccessUnitAsync(unitId.Value, d.TargetUnitId).GetAwaiter().GetResult()); + _authorizationService.CanAccessUnitAsync(unitId.Value, d.TargetUnitId).GetAwaiter().GetResult()) || + allocation.Distributions.Any(d => + _authorizationService.IsAncestorUnitAsync(d.TargetUnitId, unitId.Value).GetAwaiter().GetResult()); if (!canAccess) return Forbid(); @@ -318,6 +320,50 @@ public class AllocationsController : BaseApiController }); } + /// + /// 获取配额分配的上报历史记录 + /// + [HttpGet("distributions/{distributionId}/reports")] + public async Task GetConsumptionReports(int distributionId) + { + var unitId = GetCurrentUnitId(); + var unitLevel = GetCurrentUnitLevel(); + + if (unitId == null) + return Unauthorized(new { message = "无法获取用户组织信息" }); + + // 检查分配记录是否存在 + var distribution = await _allocationService.GetDistributionByIdAsync(distributionId); + if (distribution == null) + return NotFound(new { message = "配额分配记录不存在" }); + + // 检查访问权限: + // 1. 师团级可以查看所有记录 + // 2. 可以查看分配给自己单位的记录 + // 3. 可以查看分配给上级单位的记录 + // 4. 可以查看分配给下级单位的记录 + var canAccess = unitLevel == OrganizationalLevel.Division || + distribution.TargetUnitId == unitId.Value || + await _authorizationService.IsAncestorUnitAsync(distribution.TargetUnitId, unitId.Value) || + await _authorizationService.CanAccessUnitAsync(unitId.Value, distribution.TargetUnitId); + + if (!canAccess) + return Forbid(); + + var reports = await _allocationService.GetConsumptionReportsAsync(distributionId); + var response = reports.Select(r => new + { + id = r.Id, + reportedAmount = r.ReportedAmount, + cumulativeAmount = r.CumulativeAmount, + remarks = r.Remarks, + reportedByUserName = r.ReportedByUser?.DisplayName, + reportedAt = r.ReportedAt + }); + + return Ok(response); + } + /// /// 映射实体到响应DTO /// diff --git a/src/MilitaryTrainingManagement/Controllers/PersonnelController.cs b/src/MilitaryTrainingManagement/Controllers/PersonnelController.cs index 79f43b4..6c50110 100644 --- a/src/MilitaryTrainingManagement/Controllers/PersonnelController.cs +++ b/src/MilitaryTrainingManagement/Controllers/PersonnelController.cs @@ -91,6 +91,7 @@ public class PersonnelController : BaseApiController var items = allPersonnel .Skip((pageNumber - 1) * pageSize) .Take(pageSize) + .Select(MapToResponse) .ToList(); return Ok(new @@ -103,6 +104,45 @@ public class PersonnelController : BaseApiController }); } + /// + /// 映射人员实体到响应DTO + /// + private static PersonnelResponse MapToResponse(Personnel personnel) + { + return new PersonnelResponse + { + Id = personnel.Id, + Name = personnel.Name, + PhotoPath = personnel.PhotoPath, + Position = personnel.Position, + Rank = personnel.Rank, + Gender = personnel.Gender, + IdNumber = personnel.IdNumber, + ProfessionalTitle = personnel.ProfessionalTitle, + EducationLevel = personnel.EducationLevel, + Age = personnel.Age, + Height = personnel.Height, + ContactInfo = personnel.ContactInfo, + Hometown = personnel.Hometown, + TrainingParticipation = personnel.TrainingParticipation, + Achievements = personnel.Achievements, + SupportingDocuments = personnel.SupportingDocuments, + Ethnicity = personnel.Ethnicity, + PoliticalStatus = personnel.PoliticalStatus, + BirthDate = personnel.BirthDate, + EnlistmentDate = personnel.EnlistmentDate, + Specialty = personnel.Specialty, + SubmittedByUnitId = personnel.SubmittedByUnitId, + SubmittedByUnitName = personnel.SubmittedByUnit?.Name, + ApprovedLevel = personnel.ApprovedLevel, + ApprovedByUnitId = personnel.ApprovedByUnitId, + ApprovedByUnitName = personnel.ApprovedByUnit?.Name, + Status = personnel.Status, + SubmittedAt = personnel.SubmittedAt, + ApprovedAt = personnel.ApprovedAt + }; + } + /// /// 获取待审批的人员列表 /// @@ -143,7 +183,7 @@ public class PersonnelController : BaseApiController return Forbid(); } - return Ok(personnel); + return Ok(MapToResponse(personnel)); } /// @@ -473,7 +513,7 @@ public class PersonnelController : BaseApiController /// [HttpPost("{id}/approve")] [Authorize(Policy = "RegimentLevel")] // 团级及以上权限 - public async Task Approve(int id, [FromBody] ApprovePersonnelRequest request) + public async Task Approve(int id) { var unitId = GetCurrentUnitId(); var userId = GetCurrentUserId(); @@ -492,8 +532,9 @@ public class PersonnelController : BaseApiController try { - var personnel = await _personnelService.ApproveAsync(id, unitId.Value, request.Level); - return Ok(personnel); + // 不传递level参数,让服务层根据人员所在单位自动确定等级 + var personnel = await _personnelService.ApproveAsync(id, unitId.Value); + return Ok(MapToResponse(personnel)); } catch (ArgumentException ex) { diff --git a/src/MilitaryTrainingManagement/Controllers/StatsController.cs b/src/MilitaryTrainingManagement/Controllers/StatsController.cs index c4f2ea8..7b8f169 100644 --- a/src/MilitaryTrainingManagement/Controllers/StatsController.cs +++ b/src/MilitaryTrainingManagement/Controllers/StatsController.cs @@ -35,9 +35,22 @@ public class StatsController : BaseApiController var unitIds = await GetUnitAndSubordinateIds(unitId.Value); // 统计配额数 - var allocationsCount = await _context.MaterialAllocations - .Where(a => a.CreatedByUnitId == unitId.Value || unitIds.Contains(a.CreatedByUnitId)) - .CountAsync(); + int allocationsCount; + if (unitLevel == OrganizationalLevel.Division) + { + // 师团级:统计创建的配额数 + allocationsCount = await _context.MaterialAllocations + .Where(a => a.CreatedByUnitId == unitId.Value) + .CountAsync(); + } + else + { + // 团部及以下:统计分配给本单位及下级单位的配额数 + allocationsCount = await _context.MaterialAllocations + .Include(a => a.Distributions) + .Where(a => a.Distributions.Any(d => unitIds.Contains(d.TargetUnitId))) + .CountAsync(); + } // 统计完成率 var distributions = await _context.AllocationDistributions diff --git a/src/MilitaryTrainingManagement/Data/ApplicationDbContext.cs b/src/MilitaryTrainingManagement/Data/ApplicationDbContext.cs index 2daa25f..cabb249 100644 --- a/src/MilitaryTrainingManagement/Data/ApplicationDbContext.cs +++ b/src/MilitaryTrainingManagement/Data/ApplicationDbContext.cs @@ -22,6 +22,7 @@ public class ApplicationDbContext : DbContext public DbSet PersonnelApprovalHistories => Set(); public DbSet ApprovalRequests => Set(); public DbSet AuditLogs => Set(); + public DbSet ConsumptionReports => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -188,5 +189,24 @@ public class ApplicationDbContext : DbContext .HasForeignKey(e => e.ReviewedByUnitId) .OnDelete(DeleteBehavior.NoAction); }); + + // ConsumptionReport 配置 + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.ReportedAmount).HasPrecision(18, 2); + entity.Property(e => e.CumulativeAmount).HasPrecision(18, 2); + entity.Property(e => e.Remarks).HasMaxLength(500); + entity.HasOne(e => e.AllocationDistribution) + .WithMany() + .HasForeignKey(e => e.AllocationDistributionId) + .OnDelete(DeleteBehavior.Cascade); + entity.HasOne(e => e.ReportedByUser) + .WithMany() + .HasForeignKey(e => e.ReportedByUserId) + .OnDelete(DeleteBehavior.NoAction); + entity.HasIndex(e => e.AllocationDistributionId); + entity.HasIndex(e => e.ReportedAt); + }); } } diff --git a/src/MilitaryTrainingManagement/Migrations/20260115152942_AddConsumptionReportTable.Designer.cs b/src/MilitaryTrainingManagement/Migrations/20260115152942_AddConsumptionReportTable.Designer.cs new file mode 100644 index 0000000..1a6c704 --- /dev/null +++ b/src/MilitaryTrainingManagement/Migrations/20260115152942_AddConsumptionReportTable.Designer.cs @@ -0,0 +1,748 @@ +// +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("20260115152942_AddConsumptionReportTable")] + partial class AddConsumptionReportTable + { + /// + 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("ReportedByUserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AllocationDistributionId"); + + b.HasIndex("ReportedAt"); + + b.HasIndex("ReportedByUserId"); + + b.ToTable("ConsumptionReports"); + }); + + 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("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("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.UserAccount", "ReportedByUser") + .WithMany() + .HasForeignKey("ReportedByUserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("AllocationDistribution"); + + b.Navigation("ReportedByUser"); + }); + + 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", "SubmittedByUnit") + .WithMany() + .HasForeignKey("SubmittedByUnitId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("ApprovedByUnit"); + + 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/20260115152942_AddConsumptionReportTable.cs b/src/MilitaryTrainingManagement/Migrations/20260115152942_AddConsumptionReportTable.cs new file mode 100644 index 0000000..fd82305 --- /dev/null +++ b/src/MilitaryTrainingManagement/Migrations/20260115152942_AddConsumptionReportTable.cs @@ -0,0 +1,86 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MilitaryTrainingManagement.Migrations +{ + /// + public partial class AddConsumptionReportTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PlainPassword", + table: "UserAccounts", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.AddColumn( + name: "Unit", + table: "Personnel", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.CreateTable( + name: "ConsumptionReports", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + AllocationDistributionId = table.Column(type: "int", nullable: false), + ReportedAmount = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + CumulativeAmount = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + Remarks = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + ReportedByUserId = table.Column(type: "int", nullable: false), + ReportedAt = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ConsumptionReports", x => x.Id); + table.ForeignKey( + name: "FK_ConsumptionReports_AllocationDistributions_AllocationDistributionId", + column: x => x.AllocationDistributionId, + principalTable: "AllocationDistributions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ConsumptionReports_UserAccounts_ReportedByUserId", + column: x => x.ReportedByUserId, + principalTable: "UserAccounts", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_ConsumptionReports_AllocationDistributionId", + table: "ConsumptionReports", + column: "AllocationDistributionId"); + + migrationBuilder.CreateIndex( + name: "IX_ConsumptionReports_ReportedAt", + table: "ConsumptionReports", + column: "ReportedAt"); + + migrationBuilder.CreateIndex( + name: "IX_ConsumptionReports_ReportedByUserId", + table: "ConsumptionReports", + column: "ReportedByUserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ConsumptionReports"); + + migrationBuilder.DropColumn( + name: "PlainPassword", + table: "UserAccounts"); + + migrationBuilder.DropColumn( + name: "Unit", + table: "Personnel"); + } + } +} diff --git a/src/MilitaryTrainingManagement/Migrations/ApplicationDbContextModelSnapshot.cs b/src/MilitaryTrainingManagement/Migrations/ApplicationDbContextModelSnapshot.cs index 8a43bee..af40d11 100644 --- a/src/MilitaryTrainingManagement/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/MilitaryTrainingManagement/Migrations/ApplicationDbContextModelSnapshot.cs @@ -210,6 +210,46 @@ namespace MilitaryTrainingManagement.Migrations 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("ReportedByUserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AllocationDistributionId"); + + b.HasIndex("ReportedAt"); + + b.HasIndex("ReportedByUserId"); + + b.ToTable("ConsumptionReports"); + }); + modelBuilder.Entity("MilitaryTrainingManagement.Models.Entities.MaterialAllocation", b => { b.Property("Id") @@ -409,6 +449,9 @@ namespace MilitaryTrainingManagement.Migrations b.Property("TrainingParticipation") .HasColumnType("nvarchar(max)"); + b.Property("Unit") + .HasColumnType("nvarchar(max)"); + b.HasKey("Id"); b.HasIndex("ApprovedByUnitId"); @@ -496,6 +539,9 @@ namespace MilitaryTrainingManagement.Migrations .IsRequired() .HasColumnType("nvarchar(max)"); + b.Property("PlainPassword") + .HasColumnType("nvarchar(max)"); + b.Property("Username") .IsRequired() .HasMaxLength(50) @@ -587,6 +633,25 @@ namespace MilitaryTrainingManagement.Migrations 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.UserAccount", "ReportedByUser") + .WithMany() + .HasForeignKey("ReportedByUserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("AllocationDistribution"); + + b.Navigation("ReportedByUser"); + }); + modelBuilder.Entity("MilitaryTrainingManagement.Models.Entities.MaterialAllocation", b => { b.HasOne("MilitaryTrainingManagement.Models.Entities.OrganizationalUnit", "CreatedByUnit") diff --git a/src/MilitaryTrainingManagement/Models/Entities/ConsumptionReport.cs b/src/MilitaryTrainingManagement/Models/Entities/ConsumptionReport.cs new file mode 100644 index 0000000..fa0f11c --- /dev/null +++ b/src/MilitaryTrainingManagement/Models/Entities/ConsumptionReport.cs @@ -0,0 +1,63 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace MilitaryTrainingManagement.Models.Entities; + +/// +/// 消耗上报记录 +/// +public class ConsumptionReport +{ + [Key] + public int Id { get; set; } + + /// + /// 配额分配ID + /// + [Required] + public int AllocationDistributionId { get; set; } + + /// + /// 配额分配 + /// + [ForeignKey(nameof(AllocationDistributionId))] + public AllocationDistribution AllocationDistribution { get; set; } = null!; + + /// + /// 本次上报数量 + /// + [Required] + [Column(TypeName = "decimal(18,2)")] + public decimal ReportedAmount { get; set; } + + /// + /// 上报后累计数量 + /// + [Required] + [Column(TypeName = "decimal(18,2)")] + public decimal CumulativeAmount { get; set; } + + /// + /// 备注 + /// + [MaxLength(500)] + public string? Remarks { get; set; } + + /// + /// 上报人ID + /// + [Required] + public int ReportedByUserId { get; set; } + + /// + /// 上报人 + /// + [ForeignKey(nameof(ReportedByUserId))] + public UserAccount ReportedByUser { get; set; } = null!; + + /// + /// 上报时间 + /// + [Required] + public DateTime ReportedAt { get; set; } +} diff --git a/src/MilitaryTrainingManagement/Models/Enums/PersonnelApprovalAction.cs b/src/MilitaryTrainingManagement/Models/Enums/PersonnelApprovalAction.cs index af259da..02a0c21 100644 --- a/src/MilitaryTrainingManagement/Models/Enums/PersonnelApprovalAction.cs +++ b/src/MilitaryTrainingManagement/Models/Enums/PersonnelApprovalAction.cs @@ -28,5 +28,10 @@ public enum PersonnelApprovalAction /// /// 等级调整 /// - LevelAdjusted = 5 + LevelAdjusted = 5, + + /// + /// 向上申报等级升级 + /// + LevelUpgraded = 6 } \ No newline at end of file diff --git a/src/MilitaryTrainingManagement/Program.cs b/src/MilitaryTrainingManagement/Program.cs index 787f3c3..473ccb8 100644 --- a/src/MilitaryTrainingManagement/Program.cs +++ b/src/MilitaryTrainingManagement/Program.cs @@ -253,6 +253,39 @@ using (var scope = app.Services.CreateScope()) Console.WriteLine($"添加 UserAccounts 表 PlainPassword 列时出错: {ex.Message}"); } + // 创建 ConsumptionReports 表(如果不存在) + try + { + context.Database.ExecuteSqlRaw(@" + IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'ConsumptionReports') + BEGIN + CREATE TABLE ConsumptionReports ( + Id INT PRIMARY KEY IDENTITY(1,1), + AllocationDistributionId INT NOT NULL, + ReportedAmount DECIMAL(18,2) NOT NULL, + CumulativeAmount DECIMAL(18,2) NOT NULL, + Remarks NVARCHAR(500) NULL, + ReportedByUserId INT NOT NULL, + ReportedAt DATETIME2 NOT NULL, + CONSTRAINT FK_ConsumptionReports_AllocationDistributions + FOREIGN KEY (AllocationDistributionId) + REFERENCES AllocationDistributions(Id) ON DELETE CASCADE, + CONSTRAINT FK_ConsumptionReports_UserAccounts + FOREIGN KEY (ReportedByUserId) + REFERENCES UserAccounts(Id) + ); + CREATE INDEX IX_ConsumptionReports_AllocationDistributionId ON ConsumptionReports(AllocationDistributionId); + CREATE INDEX IX_ConsumptionReports_ReportedAt ON ConsumptionReports(ReportedAt); + CREATE INDEX IX_ConsumptionReports_ReportedByUserId ON ConsumptionReports(ReportedByUserId); + END + "); + Console.WriteLine("ConsumptionReports 表检查完成"); + } + catch (Exception ex) + { + Console.WriteLine($"创建 ConsumptionReports 表时出错: {ex.Message}"); + } + // 如果没有物资类别,创建默认类别 if (!context.MaterialCategories.Any()) { diff --git a/src/MilitaryTrainingManagement/Services/Implementations/AllocationService.cs b/src/MilitaryTrainingManagement/Services/Implementations/AllocationService.cs index e580c5f..359abe9 100644 --- a/src/MilitaryTrainingManagement/Services/Implementations/AllocationService.cs +++ b/src/MilitaryTrainingManagement/Services/Implementations/AllocationService.cs @@ -57,6 +57,14 @@ public class AllocationService : IAllocationService var subordinateIds = await _organizationService.GetAllSubordinateIdsAsync(unitId); var allUnitIds = new HashSet(subordinateIds) { unitId }; + // 获取该单位的所有上级单位ID(用于营部及以下账号查看团的配额) + var ancestorIds = await GetAncestorUnitIdsAsync(unitId); + var allRelatedUnitIds = new HashSet(allUnitIds); + foreach (var ancestorId in ancestorIds) + { + allRelatedUnitIds.Add(ancestorId); + } + // 获取分配给这些单位的配额 var allocations = await _context.MaterialAllocations .Include(a => a.CreatedByUnit) @@ -64,21 +72,38 @@ public class AllocationService : IAllocationService .ThenInclude(d => d.TargetUnit) .Include(a => a.Distributions) .ThenInclude(d => d.ReportedByUser) - .Where(a => a.Distributions.Any(d => allUnitIds.Contains(d.TargetUnitId))) + .Where(a => a.Distributions.Any(d => allRelatedUnitIds.Contains(d.TargetUnitId))) .OrderByDescending(a => a.CreatedAt) .ToListAsync(); - // 过滤每个配额的分配记录,只保留分配给当前单位及其下级的记录 + // 过滤每个配额的分配记录,只保留分配给当前单位及其上下级的记录 foreach (var allocation in allocations) { allocation.Distributions = allocation.Distributions - .Where(d => allUnitIds.Contains(d.TargetUnitId)) + .Where(d => allRelatedUnitIds.Contains(d.TargetUnitId)) .ToList(); } return allocations; } + /// + /// 获取单位的所有上级单位ID + /// + private async Task> GetAncestorUnitIdsAsync(int unitId) + { + var result = new List(); + var currentUnit = await _context.OrganizationalUnits.FindAsync(unitId); + + while (currentUnit?.ParentId != null) + { + result.Add(currentUnit.ParentId.Value); + currentUnit = await _context.OrganizationalUnits.FindAsync(currentUnit.ParentId.Value); + } + + return result; + } + public async Task> GetDistributionsForUnitAsync(int unitId) { return await _context.AllocationDistributions @@ -256,8 +281,16 @@ public class AllocationService : IAllocationService if (distribution == null) throw new ArgumentException("配额分配记录不存在"); - // 验证权限:只能更新分配给自己单位的记录 - if (distribution.TargetUnitId != unitId) + // 验证权限:可以更新分配给自己单位或上级单位的记录 + var canReport = distribution.TargetUnitId == unitId; + if (!canReport) + { + // 检查是否是上级单位的配额 + var ancestorIds = await GetAncestorUnitIdsAsync(unitId); + canReport = ancestorIds.Contains(distribution.TargetUnitId); + } + + if (!canReport) throw new UnauthorizedAccessException("无权更新此配额分配记录"); // 验证实际完成数量 @@ -267,6 +300,24 @@ public class AllocationService : IAllocationService if (actualCompletion > distribution.UnitQuota) throw new ArgumentException($"实际完成数量不能超过分配配额({distribution.UnitQuota})"); + // 计算本次上报数量 + var previousAmount = distribution.ActualCompletion ?? 0; + var reportedAmount = actualCompletion - previousAmount; + + // 如果本次上报数量大于0,保存历史记录 + if (reportedAmount > 0) + { + var consumptionReport = new ConsumptionReport + { + AllocationDistributionId = distributionId, + ReportedAmount = reportedAmount, + CumulativeAmount = actualCompletion, + ReportedByUserId = userId, + ReportedAt = DateTime.UtcNow + }; + _context.ConsumptionReports.Add(consumptionReport); + } + // 更新实际完成数量 distribution.ActualCompletion = actualCompletion; distribution.ReportedAt = DateTime.UtcNow; @@ -308,4 +359,13 @@ public class AllocationService : IAllocationService return existingIds.Count == targetUnitIds.Distinct().Count(); } + + public async Task> GetConsumptionReportsAsync(int distributionId) + { + return await _context.ConsumptionReports + .Include(r => r.ReportedByUser) + .Where(r => r.AllocationDistributionId == distributionId) + .OrderByDescending(r => r.ReportedAt) + .ToListAsync(); + } } diff --git a/src/MilitaryTrainingManagement/Services/Implementations/OrganizationService.cs b/src/MilitaryTrainingManagement/Services/Implementations/OrganizationService.cs index 9c09799..bad23e4 100644 --- a/src/MilitaryTrainingManagement/Services/Implementations/OrganizationService.cs +++ b/src/MilitaryTrainingManagement/Services/Implementations/OrganizationService.cs @@ -193,4 +193,18 @@ public class OrganizationService : IOrganizationService // 检查是否是上级单位(递归向上查找) return await IsSubordinateOfAsync(childUnitId, parentUnitId); } + + public async Task> GetAllAncestorIdsAsync(int unitId) + { + var result = new List(); + var currentUnit = await _context.OrganizationalUnits.FindAsync(unitId); + + while (currentUnit?.ParentId != null) + { + result.Add(currentUnit.ParentId.Value); + currentUnit = await _context.OrganizationalUnits.FindAsync(currentUnit.ParentId.Value); + } + + return result; + } } diff --git a/src/MilitaryTrainingManagement/Services/Implementations/PersonnelService.cs b/src/MilitaryTrainingManagement/Services/Implementations/PersonnelService.cs index dd96c3d..6dccf49 100644 --- a/src/MilitaryTrainingManagement/Services/Implementations/PersonnelService.cs +++ b/src/MilitaryTrainingManagement/Services/Implementations/PersonnelService.cs @@ -81,9 +81,11 @@ public class PersonnelService : IPersonnelService return personnel; } - public async Task ApproveAsync(int personnelId, int approvedByUnitId, PersonnelLevel level) + public async Task ApproveAsync(int personnelId, int approvedByUnitId, PersonnelLevel? level = null) { - var personnel = await _context.Personnel.FindAsync(personnelId); + var personnel = await _context.Personnel + .Include(p => p.SubmittedByUnit) + .FirstOrDefaultAsync(p => p.Id == personnelId); if (personnel == null) throw new ArgumentException("人员记录不存在"); @@ -91,9 +93,21 @@ public class PersonnelService : IPersonnelService if (approvedByUnit == null) throw new ArgumentException("审批单位不存在"); + // 人员等级变更为审批单位的等级 + PersonnelLevel actualLevel; + if (level.HasValue) + { + actualLevel = level.Value; + } + else + { + // 根据审批单位的层级确定人员等级 + actualLevel = (PersonnelLevel)(int)approvedByUnit.Level; + } + // 验证审批单位层级必须高于或等于人员等级(数值越小层级越高) var unitLevelValue = (int)approvedByUnit.Level; - var personnelLevelValue = (int)level; + var personnelLevelValue = (int)actualLevel; if (unitLevelValue > personnelLevelValue) throw new ArgumentException("审批单位层级不足以审批该等级人才"); @@ -102,23 +116,45 @@ public class PersonnelService : IPersonnelService personnel.Status = PersonnelStatus.Approved; personnel.ApprovedByUnitId = approvedByUnitId; - personnel.ApprovedLevel = level; + personnel.ApprovedLevel = actualLevel; personnel.ApprovedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); // 记录审批历史 var userId = await GetUserIdByUnitAsync(approvedByUnitId); - await RecordApprovalHistoryAsync(personnelId, PersonnelApprovalAction.Approved, - previousStatus, PersonnelStatus.Approved, previousLevel, level, - userId, approvedByUnitId, "审批通过"); + var actionType = previousStatus == PersonnelStatus.Approved + ? PersonnelApprovalAction.LevelUpgraded + : PersonnelApprovalAction.Approved; + var comments = previousStatus == PersonnelStatus.Approved + ? $"向上申报通过,等级从{GetLevelName(previousLevel)}升级为{GetLevelName(actualLevel)}" + : "审批通过"; + + await RecordApprovalHistoryAsync(personnelId, actionType, + previousStatus, PersonnelStatus.Approved, previousLevel, actualLevel, + userId, approvedByUnitId, comments); _logger.LogInformation("人员 {PersonnelId} 已被单位 {UnitId} 审批通过,等级:{Level}", - personnelId, approvedByUnitId, level); + personnelId, approvedByUnitId, actualLevel); return personnel; } + /// + /// 获取等级名称 + /// + private static string GetLevelName(PersonnelLevel? level) + { + return level switch + { + PersonnelLevel.Division => "师级人才", + PersonnelLevel.Regiment => "团级人才", + PersonnelLevel.Battalion => "营级人才", + PersonnelLevel.Company => "连级人才", + _ => "未定级" + }; + } + public async Task RejectAsync(int personnelId, int reviewedByUserId) { var personnel = await _context.Personnel.FindAsync(personnelId); @@ -519,14 +555,27 @@ public class PersonnelService : IPersonnelService var personnel = await _context.Personnel .Include(p => p.SubmittedByUnit) + .Include(p => p.ApprovedByUnit) .FirstOrDefaultAsync(p => p.Id == personnelId); - if (personnel == null || personnel.Status != PersonnelStatus.Pending) + if (personnel == null) return false; - // 同一单位可以审批自己提交的人员 + // 已拒绝的人员不能审批 + if (personnel.Status == PersonnelStatus.Rejected) + return false; + + // 本单位不能审批自己提交的人员,必须由上级单位审批 if (user.OrganizationalUnitId == personnel.SubmittedByUnitId) - return true; + return false; + + // 如果是已审批的人员,检查当前用户单位是否比已审批单位层级更高 + if (personnel.Status == PersonnelStatus.Approved && personnel.ApprovedByUnitId.HasValue) + { + // 用户单位层级必须高于已审批单位层级(数值越小层级越高) + if ((int)user.OrganizationalUnit!.Level >= (int)personnel.ApprovedByUnit!.Level) + return false; + } // 检查用户的组织单位是否是提交单位的上级 var isParent = await _organizationService.IsParentUnitAsync(user.OrganizationalUnitId, personnel.SubmittedByUnitId); diff --git a/src/MilitaryTrainingManagement/Services/Interfaces/IAllocationService.cs b/src/MilitaryTrainingManagement/Services/Interfaces/IAllocationService.cs index e72881b..13d23c6 100644 --- a/src/MilitaryTrainingManagement/Services/Interfaces/IAllocationService.cs +++ b/src/MilitaryTrainingManagement/Services/Interfaces/IAllocationService.cs @@ -71,4 +71,9 @@ public interface IAllocationService /// 验证目标单位是否存在 /// Task ValidateTargetUnitsExistAsync(IEnumerable targetUnitIds); + + /// + /// 获取配额分配的上报历史记录 + /// + Task> GetConsumptionReportsAsync(int distributionId); } diff --git a/src/MilitaryTrainingManagement/Services/Interfaces/IOrganizationService.cs b/src/MilitaryTrainingManagement/Services/Interfaces/IOrganizationService.cs index fc77ca0..82814f7 100644 --- a/src/MilitaryTrainingManagement/Services/Interfaces/IOrganizationService.cs +++ b/src/MilitaryTrainingManagement/Services/Interfaces/IOrganizationService.cs @@ -12,6 +12,7 @@ public interface IOrganizationService Task> GetAllAsync(); Task> GetSubordinatesAsync(int unitId); Task> GetAllSubordinateIdsAsync(int unitId); + Task> GetAllAncestorIdsAsync(int unitId); Task CreateAsync(string name, OrganizationalLevel level, int? parentId); Task UpdateAsync(int id, string name); Task DeleteAsync(int id); diff --git a/src/MilitaryTrainingManagement/Services/Interfaces/IPersonnelService.cs b/src/MilitaryTrainingManagement/Services/Interfaces/IPersonnelService.cs index 5af947d..7a6e855 100644 --- a/src/MilitaryTrainingManagement/Services/Interfaces/IPersonnelService.cs +++ b/src/MilitaryTrainingManagement/Services/Interfaces/IPersonnelService.cs @@ -12,7 +12,7 @@ public interface IPersonnelService Task> GetByUnitAsync(int unitId, bool includeSubordinates = false); Task CreateAsync(Personnel personnel); Task UpdateAsync(Personnel personnel); - Task ApproveAsync(int personnelId, int approvedByUnitId, PersonnelLevel level); + Task ApproveAsync(int personnelId, int approvedByUnitId, PersonnelLevel? level = null); Task RejectAsync(int personnelId, int reviewedByUserId); Task DeleteAsync(int id); diff --git a/src/MilitaryTrainingManagement/create_consumption_reports.sql b/src/MilitaryTrainingManagement/create_consumption_reports.sql new file mode 100644 index 0000000..1764b76 --- /dev/null +++ b/src/MilitaryTrainingManagement/create_consumption_reports.sql @@ -0,0 +1,47 @@ +-- 创建 ConsumptionReports 表 +IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[ConsumptionReports]') AND type in (N'U')) +BEGIN + CREATE TABLE [dbo].[ConsumptionReports]( + [Id] [int] IDENTITY(1,1) NOT NULL, + [AllocationDistributionId] [int] NOT NULL, + [ReportedAmount] [decimal](18, 2) NOT NULL, + [CumulativeAmount] [decimal](18, 2) NOT NULL, + [Remarks] [nvarchar](500) NULL, + [ReportedByUserId] [int] NOT NULL, + [ReportedAt] [datetime2](7) NOT NULL, + CONSTRAINT [PK_ConsumptionReports] PRIMARY KEY CLUSTERED ([Id] ASC) + ) + + CREATE NONCLUSTERED INDEX [IX_ConsumptionReports_AllocationDistributionId] ON [dbo].[ConsumptionReports] + ( + [AllocationDistributionId] ASC + ) + + CREATE NONCLUSTERED INDEX [IX_ConsumptionReports_ReportedAt] ON [dbo].[ConsumptionReports] + ( + [ReportedAt] ASC + ) + + CREATE NONCLUSTERED INDEX [IX_ConsumptionReports_ReportedByUserId] ON [dbo].[ConsumptionReports] + ( + [ReportedByUserId] ASC + ) + + ALTER TABLE [dbo].[ConsumptionReports] WITH CHECK ADD CONSTRAINT [FK_ConsumptionReports_AllocationDistributions_AllocationDistributionId] FOREIGN KEY([AllocationDistributionId]) + REFERENCES [dbo].[AllocationDistributions] ([Id]) + ON DELETE CASCADE + + ALTER TABLE [dbo].[ConsumptionReports] CHECK CONSTRAINT [FK_ConsumptionReports_AllocationDistributions_AllocationDistributionId] + + ALTER TABLE [dbo].[ConsumptionReports] WITH CHECK ADD CONSTRAINT [FK_ConsumptionReports_UserAccounts_ReportedByUserId] FOREIGN KEY([ReportedByUserId]) + REFERENCES [dbo].[UserAccounts] ([Id]) + + ALTER TABLE [dbo].[ConsumptionReports] CHECK CONSTRAINT [FK_ConsumptionReports_UserAccounts_ReportedByUserId] + + PRINT 'ConsumptionReports table created successfully' +END +ELSE +BEGIN + PRINT 'ConsumptionReports table already exists' +END +GO diff --git a/src/MilitaryTrainingManagement/migration.sql b/src/MilitaryTrainingManagement/migration.sql new file mode 100644 index 0000000..323c122 --- /dev/null +++ b/src/MilitaryTrainingManagement/migration.sql @@ -0,0 +1,622 @@ +IF OBJECT_ID(N'[__EFMigrationsHistory]') IS NULL +BEGIN + CREATE TABLE [__EFMigrationsHistory] ( + [MigrationId] nvarchar(150) NOT NULL, + [ProductVersion] nvarchar(32) NOT NULL, + CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId]) + ); +END; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE TABLE [MaterialCategories] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(50) NOT NULL, + [Description] nvarchar(200) NULL, + [IsActive] bit NOT NULL, + [CreatedAt] datetime2 NOT NULL, + [SortOrder] int NOT NULL, + CONSTRAINT [PK_MaterialCategories] PRIMARY KEY ([Id]) + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE TABLE [OrganizationalUnits] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(100) NOT NULL, + [Level] int NOT NULL, + [ParentId] int NULL, + [CreatedAt] datetime2 NOT NULL, + CONSTRAINT [PK_OrganizationalUnits] PRIMARY KEY ([Id]), + CONSTRAINT [FK_OrganizationalUnits_OrganizationalUnits_ParentId] FOREIGN KEY ([ParentId]) REFERENCES [OrganizationalUnits] ([Id]) ON DELETE NO ACTION + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE TABLE [MaterialAllocations] ( + [Id] int NOT NULL IDENTITY, + [Category] nvarchar(100) NOT NULL, + [MaterialName] nvarchar(200) NOT NULL, + [Unit] nvarchar(50) NOT NULL, + [TotalQuota] decimal(18,2) NOT NULL, + [CreatedByUnitId] int NOT NULL, + [CreatedAt] datetime2 NOT NULL, + CONSTRAINT [PK_MaterialAllocations] PRIMARY KEY ([Id]), + CONSTRAINT [FK_MaterialAllocations_OrganizationalUnits_CreatedByUnitId] FOREIGN KEY ([CreatedByUnitId]) REFERENCES [OrganizationalUnits] ([Id]) ON DELETE NO ACTION + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE TABLE [Personnel] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(50) NOT NULL, + [PhotoPath] nvarchar(max) NULL, + [Position] nvarchar(100) NOT NULL, + [Rank] nvarchar(50) NOT NULL, + [Gender] nvarchar(10) NOT NULL, + [IdNumber] nvarchar(18) NOT NULL, + [ProfessionalTitle] nvarchar(max) NULL, + [EducationLevel] nvarchar(max) NULL, + [Age] int NOT NULL, + [Height] decimal(5,2) NULL, + [ContactInfo] nvarchar(max) NULL, + [Hometown] nvarchar(max) NULL, + [TrainingParticipation] nvarchar(max) NULL, + [Achievements] nvarchar(max) NULL, + [SupportingDocuments] nvarchar(max) NULL, + [SubmittedByUnitId] int NOT NULL, + [ApprovedLevel] int NULL, + [ApprovedByUnitId] int NULL, + [Status] int NOT NULL, + [SubmittedAt] datetime2 NOT NULL, + [ApprovedAt] datetime2 NULL, + CONSTRAINT [PK_Personnel] PRIMARY KEY ([Id]), + CONSTRAINT [FK_Personnel_OrganizationalUnits_ApprovedByUnitId] FOREIGN KEY ([ApprovedByUnitId]) REFERENCES [OrganizationalUnits] ([Id]), + CONSTRAINT [FK_Personnel_OrganizationalUnits_SubmittedByUnitId] FOREIGN KEY ([SubmittedByUnitId]) REFERENCES [OrganizationalUnits] ([Id]) + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE TABLE [UserAccounts] ( + [Id] int NOT NULL IDENTITY, + [Username] nvarchar(50) NOT NULL, + [PasswordHash] nvarchar(max) NOT NULL, + [DisplayName] nvarchar(100) NOT NULL, + [OrganizationalUnitId] int NOT NULL, + [IsActive] bit NOT NULL, + [CreatedAt] datetime2 NOT NULL, + [LastLoginAt] datetime2 NULL, + CONSTRAINT [PK_UserAccounts] PRIMARY KEY ([Id]), + CONSTRAINT [FK_UserAccounts_OrganizationalUnits_OrganizationalUnitId] FOREIGN KEY ([OrganizationalUnitId]) REFERENCES [OrganizationalUnits] ([Id]) ON DELETE NO ACTION + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE TABLE [AllocationDistributions] ( + [Id] int NOT NULL IDENTITY, + [AllocationId] int NOT NULL, + [TargetUnitId] int NOT NULL, + [UnitQuota] decimal(18,2) NOT NULL, + [ActualCompletion] decimal(18,2) NULL, + [ReportedAt] datetime2 NULL, + [ReportedByUserId] int NULL, + [IsApproved] bit NOT NULL, + [ApprovedAt] datetime2 NULL, + [ApprovedByUserId] int NULL, + CONSTRAINT [PK_AllocationDistributions] PRIMARY KEY ([Id]), + CONSTRAINT [FK_AllocationDistributions_MaterialAllocations_AllocationId] FOREIGN KEY ([AllocationId]) REFERENCES [MaterialAllocations] ([Id]) ON DELETE CASCADE, + CONSTRAINT [FK_AllocationDistributions_OrganizationalUnits_TargetUnitId] FOREIGN KEY ([TargetUnitId]) REFERENCES [OrganizationalUnits] ([Id]), + CONSTRAINT [FK_AllocationDistributions_UserAccounts_ApprovedByUserId] FOREIGN KEY ([ApprovedByUserId]) REFERENCES [UserAccounts] ([Id]), + CONSTRAINT [FK_AllocationDistributions_UserAccounts_ReportedByUserId] FOREIGN KEY ([ReportedByUserId]) REFERENCES [UserAccounts] ([Id]) + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE TABLE [ApprovalRequests] ( + [Id] int NOT NULL IDENTITY, + [Type] int NOT NULL, + [TargetEntityId] int NOT NULL, + [RequestedByUserId] int NOT NULL, + [RequestedByUnitId] int NOT NULL, + [Reason] nvarchar(500) NOT NULL, + [OriginalData] nvarchar(max) NOT NULL, + [RequestedChanges] nvarchar(max) NOT NULL, + [Status] int NOT NULL, + [ReviewedByUserId] int NULL, + [ReviewComments] nvarchar(max) NULL, + [RequestedAt] datetime2 NOT NULL, + [ReviewedAt] datetime2 NULL, + CONSTRAINT [PK_ApprovalRequests] PRIMARY KEY ([Id]), + CONSTRAINT [FK_ApprovalRequests_OrganizationalUnits_RequestedByUnitId] FOREIGN KEY ([RequestedByUnitId]) REFERENCES [OrganizationalUnits] ([Id]), + CONSTRAINT [FK_ApprovalRequests_UserAccounts_RequestedByUserId] FOREIGN KEY ([RequestedByUserId]) REFERENCES [UserAccounts] ([Id]), + CONSTRAINT [FK_ApprovalRequests_UserAccounts_ReviewedByUserId] FOREIGN KEY ([ReviewedByUserId]) REFERENCES [UserAccounts] ([Id]) + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE TABLE [AuditLogs] ( + [Id] int NOT NULL IDENTITY, + [EntityType] nvarchar(100) NOT NULL, + [EntityId] int NOT NULL, + [Action] nvarchar(50) NOT NULL, + [Description] nvarchar(500) NULL, + [OldValues] nvarchar(max) NULL, + [NewValues] nvarchar(max) NULL, + [ChangedFields] nvarchar(max) NULL, + [UserId] int NULL, + [OrganizationalUnitId] int NULL, + [Timestamp] datetime2 NOT NULL, + [IpAddress] nvarchar(50) NULL, + [UserAgent] nvarchar(500) NULL, + [RequestPath] nvarchar(500) NULL, + [IsSuccess] bit NOT NULL, + [ErrorMessage] nvarchar(2000) NULL, + CONSTRAINT [PK_AuditLogs] PRIMARY KEY ([Id]), + CONSTRAINT [FK_AuditLogs_OrganizationalUnits_OrganizationalUnitId] FOREIGN KEY ([OrganizationalUnitId]) REFERENCES [OrganizationalUnits] ([Id]), + CONSTRAINT [FK_AuditLogs_UserAccounts_UserId] FOREIGN KEY ([UserId]) REFERENCES [UserAccounts] ([Id]) + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE TABLE [PersonnelApprovalHistories] ( + [Id] int NOT NULL IDENTITY, + [PersonnelId] int NOT NULL, + [Action] int NOT NULL, + [PreviousStatus] int NULL, + [NewStatus] int NOT NULL, + [PreviousLevel] int NULL, + [NewLevel] int NULL, + [ReviewedByUserId] int NOT NULL, + [ReviewedByUnitId] int NULL, + [Comments] nvarchar(max) NULL, + [ReviewedAt] datetime2 NOT NULL, + CONSTRAINT [PK_PersonnelApprovalHistories] PRIMARY KEY ([Id]), + CONSTRAINT [FK_PersonnelApprovalHistories_OrganizationalUnits_ReviewedByUnitId] FOREIGN KEY ([ReviewedByUnitId]) REFERENCES [OrganizationalUnits] ([Id]), + CONSTRAINT [FK_PersonnelApprovalHistories_Personnel_PersonnelId] FOREIGN KEY ([PersonnelId]) REFERENCES [Personnel] ([Id]) ON DELETE CASCADE, + CONSTRAINT [FK_PersonnelApprovalHistories_UserAccounts_ReviewedByUserId] FOREIGN KEY ([ReviewedByUserId]) REFERENCES [UserAccounts] ([Id]) + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE INDEX [IX_AllocationDistributions_AllocationId] ON [AllocationDistributions] ([AllocationId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE INDEX [IX_AllocationDistributions_ApprovedByUserId] ON [AllocationDistributions] ([ApprovedByUserId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE INDEX [IX_AllocationDistributions_ReportedByUserId] ON [AllocationDistributions] ([ReportedByUserId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE INDEX [IX_AllocationDistributions_TargetUnitId] ON [AllocationDistributions] ([TargetUnitId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE INDEX [IX_ApprovalRequests_RequestedByUnitId] ON [ApprovalRequests] ([RequestedByUnitId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE INDEX [IX_ApprovalRequests_RequestedByUserId] ON [ApprovalRequests] ([RequestedByUserId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE INDEX [IX_ApprovalRequests_ReviewedByUserId] ON [ApprovalRequests] ([ReviewedByUserId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE INDEX [IX_AuditLogs_Action] ON [AuditLogs] ([Action]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE INDEX [IX_AuditLogs_EntityId] ON [AuditLogs] ([EntityId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE INDEX [IX_AuditLogs_EntityType] ON [AuditLogs] ([EntityType]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE INDEX [IX_AuditLogs_OrganizationalUnitId] ON [AuditLogs] ([OrganizationalUnitId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE INDEX [IX_AuditLogs_Timestamp] ON [AuditLogs] ([Timestamp]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE INDEX [IX_AuditLogs_UserId] ON [AuditLogs] ([UserId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE INDEX [IX_MaterialAllocations_CreatedByUnitId] ON [MaterialAllocations] ([CreatedByUnitId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE UNIQUE INDEX [IX_MaterialCategories_Name] ON [MaterialCategories] ([Name]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE INDEX [IX_OrganizationalUnits_ParentId] ON [OrganizationalUnits] ([ParentId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE INDEX [IX_Personnel_ApprovedByUnitId] ON [Personnel] ([ApprovedByUnitId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE UNIQUE INDEX [IX_Personnel_IdNumber] ON [Personnel] ([IdNumber]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE INDEX [IX_Personnel_SubmittedByUnitId] ON [Personnel] ([SubmittedByUnitId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE INDEX [IX_PersonnelApprovalHistories_PersonnelId] ON [PersonnelApprovalHistories] ([PersonnelId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE INDEX [IX_PersonnelApprovalHistories_ReviewedByUnitId] ON [PersonnelApprovalHistories] ([ReviewedByUnitId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE INDEX [IX_PersonnelApprovalHistories_ReviewedByUserId] ON [PersonnelApprovalHistories] ([ReviewedByUserId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE INDEX [IX_UserAccounts_OrganizationalUnitId] ON [UserAccounts] ([OrganizationalUnitId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + CREATE UNIQUE INDEX [IX_UserAccounts_Username] ON [UserAccounts] ([Username]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260114141545_AddMaterialCategory' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260114141545_AddMaterialCategory', N'8.0.0'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260115063457_UpdatePersonnelFieldsComplete' +) +BEGIN + DROP INDEX [IX_Personnel_IdNumber] ON [Personnel]; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260115063457_UpdatePersonnelFieldsComplete' +) +BEGIN + DECLARE @var0 sysname; + SELECT @var0 = [d].[name] + FROM [sys].[default_constraints] [d] + INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] + WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Personnel]') AND [c].[name] = N'IdNumber'); + IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [Personnel] DROP CONSTRAINT [' + @var0 + '];'); + ALTER TABLE [Personnel] ALTER COLUMN [IdNumber] nvarchar(50) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260115063457_UpdatePersonnelFieldsComplete' +) +BEGIN + ALTER TABLE [Personnel] ADD [BirthDate] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260115063457_UpdatePersonnelFieldsComplete' +) +BEGIN + ALTER TABLE [Personnel] ADD [EnlistmentDate] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260115063457_UpdatePersonnelFieldsComplete' +) +BEGIN + ALTER TABLE [Personnel] ADD [Ethnicity] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260115063457_UpdatePersonnelFieldsComplete' +) +BEGIN + ALTER TABLE [Personnel] ADD [PoliticalStatus] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260115063457_UpdatePersonnelFieldsComplete' +) +BEGIN + ALTER TABLE [Personnel] ADD [Specialty] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260115063457_UpdatePersonnelFieldsComplete' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260115063457_UpdatePersonnelFieldsComplete', N'8.0.0'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260115152942_AddConsumptionReportTable' +) +BEGIN + ALTER TABLE [UserAccounts] ADD [PlainPassword] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260115152942_AddConsumptionReportTable' +) +BEGIN + ALTER TABLE [Personnel] ADD [Unit] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260115152942_AddConsumptionReportTable' +) +BEGIN + CREATE TABLE [ConsumptionReports] ( + [Id] int NOT NULL IDENTITY, + [AllocationDistributionId] int NOT NULL, + [ReportedAmount] decimal(18,2) NOT NULL, + [CumulativeAmount] decimal(18,2) NOT NULL, + [Remarks] nvarchar(500) NULL, + [ReportedByUserId] int NOT NULL, + [ReportedAt] datetime2 NOT NULL, + CONSTRAINT [PK_ConsumptionReports] PRIMARY KEY ([Id]), + CONSTRAINT [FK_ConsumptionReports_AllocationDistributions_AllocationDistributionId] FOREIGN KEY ([AllocationDistributionId]) REFERENCES [AllocationDistributions] ([Id]) ON DELETE CASCADE, + CONSTRAINT [FK_ConsumptionReports_UserAccounts_ReportedByUserId] FOREIGN KEY ([ReportedByUserId]) REFERENCES [UserAccounts] ([Id]) + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260115152942_AddConsumptionReportTable' +) +BEGIN + CREATE INDEX [IX_ConsumptionReports_AllocationDistributionId] ON [ConsumptionReports] ([AllocationDistributionId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260115152942_AddConsumptionReportTable' +) +BEGIN + CREATE INDEX [IX_ConsumptionReports_ReportedAt] ON [ConsumptionReports] ([ReportedAt]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260115152942_AddConsumptionReportTable' +) +BEGIN + CREATE INDEX [IX_ConsumptionReports_ReportedByUserId] ON [ConsumptionReports] ([ReportedByUserId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260115152942_AddConsumptionReportTable' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260115152942_AddConsumptionReportTable', N'8.0.0'); +END; +GO + +COMMIT; +GO + diff --git a/src/frontend/src/api/allocations.ts b/src/frontend/src/api/allocations.ts index 124c810..6616fbc 100644 --- a/src/frontend/src/api/allocations.ts +++ b/src/frontend/src/api/allocations.ts @@ -46,5 +46,19 @@ export const allocationsApi = { async getMyDistributions(): Promise { const response = await apiClient.get('/allocations/my-distributions') return response.data + }, + + async getConsumptionReports(distributionId: number): Promise { + const response = await apiClient.get(`/allocations/distributions/${distributionId}/reports`) + return response.data } } + +export interface ConsumptionReport { + id: number + reportedAmount: number + cumulativeAmount: number + remarks?: string + reportedByUserName?: string + reportedAt: string +} diff --git a/src/frontend/src/api/personnel.ts b/src/frontend/src/api/personnel.ts index 5098959..b451364 100644 --- a/src/frontend/src/api/personnel.ts +++ b/src/frontend/src/api/personnel.ts @@ -40,9 +40,7 @@ export const personnelApi = { }, async approve(data: PersonnelApprovalRequest): Promise { - const response = await apiClient.post(`/personnel/${data.personnelId}/approve`, { - level: data.level - }) + const response = await apiClient.post(`/personnel/${data.personnelId}/approve`) return response.data }, diff --git a/src/frontend/src/views/allocations/AllocationList.vue b/src/frontend/src/views/allocations/AllocationList.vue index 62d8ab7..30b6924 100644 --- a/src/frontend/src/views/allocations/AllocationList.vue +++ b/src/frontend/src/views/allocations/AllocationList.vue @@ -63,12 +63,21 @@ {{ row.unit }} - + + - + + + + + - + + + + - +