[CLAUDE] PurchaseEvaluation: Mig 57 ghi chu gia de xuat PRO/CCM + so phan cach VND + sua chinh ta + guard #70
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m52s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m52s
Theo Tra Sol + anh Kiet FDC (Zalo): A) o nhap tien VndInlineEdit + BudgetCell nhay phan cach vi-VN (300.000.000) on-keystroke (o dialog da co san). B) them o ghi chu PRO + CCM canh nut Luu trong khoi Gia de xuat (giai thich vi sao chon Min/Max) — Mig 57 AddPeSuggestedPriceNotes (+ProSuggestedPriceNote +CcmSuggestedPriceNote nvarchar(1000) null, additive no-backfill no-table); 2 setter command +Note absolute-set rides role-gate PRO/CCM/Admin; DTO +2 field; controller body +note. C) sua chinh ta 'd. Ban so sanh' -> 'd. Bang so sanh gia' (2 app). GUARD gotcha #70: o gia + ghi chu echo nhau absolute-set -> them peFetching khoa nut Luu toi khi pe-detail refetch land, tranh stale-echo mat du lieu (mirror bang ngan sach S76; em-main review bat impl-frontend sot guard). BE slnx 0-warn 0-err; FE build PASS x2; test 344->351 (+7); PeDetailTabs/PeWorkspaceCreateView 2 app SHA256-identical. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -102,18 +102,18 @@ public class PurchaseEvaluationsController(IMediator mediator) : ControllerBase
|
||||
[HttpPut("{id:guid}/suggested-price/pro")]
|
||||
public async Task<IActionResult> SetSuggestedPricePro(Guid id, [FromBody] SuggestedPriceProBody body, CancellationToken ct)
|
||||
{
|
||||
await mediator.Send(new UpdatePeSuggestedPriceProCommand(id, body.MinPrice, body.MaxPrice), ct);
|
||||
await mediator.Send(new UpdatePeSuggestedPriceProCommand(id, body.MinPrice, body.MaxPrice, body.Note), ct);
|
||||
return NoContent();
|
||||
}
|
||||
public record SuggestedPriceProBody(decimal? MinPrice, decimal? MaxPrice);
|
||||
public record SuggestedPriceProBody(decimal? MinPrice, decimal? MaxPrice, string? Note);
|
||||
|
||||
[HttpPut("{id:guid}/suggested-price/ccm")]
|
||||
public async Task<IActionResult> SetSuggestedPriceCcm(Guid id, [FromBody] SuggestedPriceCcmBody body, CancellationToken ct)
|
||||
{
|
||||
await mediator.Send(new UpdatePeSuggestedPriceCcmCommand(id, body.CcmPrice), ct);
|
||||
await mediator.Send(new UpdatePeSuggestedPriceCcmCommand(id, body.CcmPrice, body.Note), ct);
|
||||
return NoContent();
|
||||
}
|
||||
public record SuggestedPriceCcmBody(decimal? CcmPrice);
|
||||
public record SuggestedPriceCcmBody(decimal? CcmPrice, string? Note);
|
||||
|
||||
[HttpPost("{id:guid}/transitions")]
|
||||
public async Task<IActionResult> Transition(Guid id, [FromBody] TransitionPeBody body, CancellationToken ct)
|
||||
|
||||
@ -257,6 +257,10 @@ public record PurchaseEvaluationDetailBundleDto(
|
||||
decimal? ProSuggestedMinPrice,
|
||||
decimal? ProSuggestedMaxPrice,
|
||||
decimal? CcmSuggestedPrice,
|
||||
// [Mig 57 2026-06-19] Ghi chú giải thích giá đề xuất — vì sao min vs max (PRO) / 1 giá
|
||||
// (CCM). FE render <Textarea> kế ô giá, set cùng endpoint suggested-price/{pro,ccm}.
|
||||
string? ProSuggestedPriceNote,
|
||||
string? CcmSuggestedPriceNote,
|
||||
decimal? ApprovedPriceAmount,
|
||||
string? ApprovedPriceSource,
|
||||
bool CanEditProSuggestedPrice,
|
||||
|
||||
@ -25,7 +25,8 @@ namespace SolutionErp.Application.PurchaseEvaluations;
|
||||
public record UpdatePeSuggestedPriceProCommand(
|
||||
Guid PeId,
|
||||
decimal? MinPrice,
|
||||
decimal? MaxPrice) : IRequest;
|
||||
decimal? MaxPrice,
|
||||
string? Note = null) : IRequest; // [Mig 57] ghi chú giải thích min vs max
|
||||
|
||||
public class UpdatePeSuggestedPriceProCommandValidator : AbstractValidator<UpdatePeSuggestedPriceProCommand>
|
||||
{
|
||||
@ -37,6 +38,7 @@ public class UpdatePeSuggestedPriceProCommandValidator : AbstractValidator<Updat
|
||||
RuleFor(x => x).Must(x => x.MinPrice!.Value <= x.MaxPrice!.Value)
|
||||
.When(x => x.MinPrice.HasValue && x.MaxPrice.HasValue)
|
||||
.WithMessage("Giá Min phải ≤ Giá Max.");
|
||||
RuleFor(x => x.Note).MaximumLength(1000); // [Mig 57] MATCH EF HasMaxLength(1000)
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,14 +61,18 @@ public class UpdatePeSuggestedPriceProCommandHandler(
|
||||
|
||||
var oldMin = pe.ProSuggestedMinPrice;
|
||||
var oldMax = pe.ProSuggestedMaxPrice;
|
||||
var oldNote = pe.ProSuggestedPriceNote;
|
||||
pe.ProSuggestedMinPrice = request.MinPrice; // absolute-set (null = clear)
|
||||
pe.ProSuggestedMaxPrice = request.MaxPrice;
|
||||
pe.ProSuggestedPriceNote = request.Note; // [Mig 57] absolute-set (null = clear)
|
||||
|
||||
var parts = new List<string>();
|
||||
if (oldMin != request.MinPrice)
|
||||
parts.Add($"giá Min {oldMin?.ToString("N0") ?? "(trống)"}đ → {request.MinPrice?.ToString("N0") ?? "(trống)"}đ");
|
||||
if (oldMax != request.MaxPrice)
|
||||
parts.Add($"giá Max {oldMax?.ToString("N0") ?? "(trống)"}đ → {request.MaxPrice?.ToString("N0") ?? "(trống)"}đ");
|
||||
if (oldNote != request.Note)
|
||||
parts.Add("ghi chú giá");
|
||||
|
||||
db.PurchaseEvaluationChangelogs.Add(new PurchaseEvaluationChangelog
|
||||
{
|
||||
@ -87,13 +93,15 @@ public class UpdatePeSuggestedPriceProCommandHandler(
|
||||
|
||||
public record UpdatePeSuggestedPriceCcmCommand(
|
||||
Guid PeId,
|
||||
decimal? CcmPrice) : IRequest;
|
||||
decimal? CcmPrice,
|
||||
string? Note = null) : IRequest; // [Mig 57] ghi chú giải thích giá CCM
|
||||
|
||||
public class UpdatePeSuggestedPriceCcmCommandValidator : AbstractValidator<UpdatePeSuggestedPriceCcmCommand>
|
||||
{
|
||||
public UpdatePeSuggestedPriceCcmCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.CcmPrice).GreaterThanOrEqualTo(0).When(x => x.CcmPrice.HasValue);
|
||||
RuleFor(x => x.Note).MaximumLength(1000); // [Mig 57] MATCH EF HasMaxLength(1000)
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,8 +122,11 @@ public class UpdatePeSuggestedPriceCcmCommandHandler(
|
||||
}
|
||||
|
||||
var oldCcm = pe.CcmSuggestedPrice;
|
||||
pe.CcmSuggestedPrice = request.CcmPrice; // absolute-set (null = clear)
|
||||
var oldNote = pe.CcmSuggestedPriceNote;
|
||||
pe.CcmSuggestedPrice = request.CcmPrice; // absolute-set (null = clear)
|
||||
pe.CcmSuggestedPriceNote = request.Note; // [Mig 57] absolute-set (null = clear)
|
||||
|
||||
var noteChanged = oldNote != request.Note ? " (kèm ghi chú)" : "";
|
||||
db.PurchaseEvaluationChangelogs.Add(new PurchaseEvaluationChangelog
|
||||
{
|
||||
PurchaseEvaluationId = pe.Id,
|
||||
@ -124,7 +135,7 @@ public class UpdatePeSuggestedPriceCcmCommandHandler(
|
||||
PhaseAtChange = pe.Phase,
|
||||
UserId = currentUser.UserId,
|
||||
UserName = currentUser.FullName ?? currentUser.Email,
|
||||
Summary = $"Giá đề xuất (CCM): {oldCcm?.ToString("N0") ?? "(trống)"}đ → {request.CcmPrice?.ToString("N0") ?? "(trống)"}đ",
|
||||
Summary = $"Giá đề xuất (CCM): {oldCcm?.ToString("N0") ?? "(trống)"}đ → {request.CcmPrice?.ToString("N0") ?? "(trống)"}đ{noteChanged}",
|
||||
});
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
@ -1098,6 +1098,7 @@ public class GetPurchaseEvaluationQueryHandler(
|
||||
e.BudgetPeriodAmount, e.ExpectedRemainingAmount, peBudgetSummary,
|
||||
e.IsUrgentByPro, e.IsUrgentByCcm, winnerQuoteTotal, awCeoThreshold, // [S69] cờ gấp + giá trị gói + ngưỡng CEO
|
||||
e.ProSuggestedMinPrice, e.ProSuggestedMaxPrice, e.CcmSuggestedPrice, // [Mig 54] giá đề xuất PRO/CCM
|
||||
e.ProSuggestedPriceNote, e.CcmSuggestedPriceNote, // [Mig 57] ghi chú giải thích giá
|
||||
e.ApprovedPriceAmount, e.ApprovedPriceSource, // [Mig 54] giá chốt người duyệt chọn
|
||||
canEditProSuggested, canEditCcmSuggested, // [Mig 54] capability role-gate
|
||||
e.ApprovalWorkflowId, awCode, awName, awVersion, currentLevelOptions,
|
||||
|
||||
@ -74,6 +74,14 @@ public class PurchaseEvaluation : AuditableEntity
|
||||
public decimal? ProSuggestedMaxPrice { get; set; }
|
||||
public decimal? CcmSuggestedPrice { get; set; }
|
||||
|
||||
// [Mig 57 2026-06-19 — anh Kiệt FDC + Tra Sol] Ghi chú GIẢI THÍCH giá đề xuất — vì
|
||||
// dải Min/Max của PRO (và 1 giá CCM) cần ghi rõ LÝ DO chọn min vs max để CEO duyệt
|
||||
// có ngữ cảnh. Mirror ProSuggestedPriceNote ↔ PRO, CcmSuggestedPriceNote ↔ CCM. Set
|
||||
// absolute (null=clear) trong cùng handler nhập giá, role-gate y như giá. Nullable,
|
||||
// max 1000. KHÔNG entity con / bảng mới (mirror HoSoLink + CcmNote pattern).
|
||||
public string? ProSuggestedPriceNote { get; set; }
|
||||
public string? CcmSuggestedPriceNote { get; set; }
|
||||
|
||||
// [Mig 54] Giá CHỐT người duyệt cấp cuối chọn khi duyệt (① "duyệt theo giá đề xuất").
|
||||
// Set tại MỌI nhánh DaDuyet của ApproveV2Async (terminal + CCM-delegation-finalize).
|
||||
// ApprovedPriceSource ∈ {Ncc, ProMin, ProMax, Ccm} = nguồn giá đã chọn; Amount =
|
||||
|
||||
@ -31,6 +31,10 @@ public class PurchaseEvaluationConfiguration : IEntityTypeConfiguration<Purchase
|
||||
b.Property(x => x.CcmSuggestedPrice).HasPrecision(18, 2);
|
||||
b.Property(x => x.ApprovedPriceAmount).HasPrecision(18, 2);
|
||||
b.Property(x => x.ApprovedPriceSource).HasMaxLength(20);
|
||||
// [Mig 57 2026-06-19] Ghi chú giải thích giá đề xuất PRO/CCM — nvarchar(1000)
|
||||
// nullable, KHÔNG index (free-text). Mirror HoSoLink + PeWorkItemBudget.CcmNote.
|
||||
b.Property(x => x.ProSuggestedPriceNote).HasMaxLength(1000);
|
||||
b.Property(x => x.CcmSuggestedPriceNote).HasMaxLength(1000);
|
||||
|
||||
b.HasIndex(x => x.MaPhieu).IsUnique().HasFilter("[MaPhieu] IS NOT NULL");
|
||||
b.HasIndex(x => new { x.Phase, x.IsDeleted });
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,40 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddPeSuggestedPriceNotes : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "CcmSuggestedPriceNote",
|
||||
table: "PurchaseEvaluations",
|
||||
type: "nvarchar(1000)",
|
||||
maxLength: 1000,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ProSuggestedPriceNote",
|
||||
table: "PurchaseEvaluations",
|
||||
type: "nvarchar(1000)",
|
||||
maxLength: 1000,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CcmSuggestedPriceNote",
|
||||
table: "PurchaseEvaluations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ProSuggestedPriceNote",
|
||||
table: "PurchaseEvaluations");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4610,6 +4610,10 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<string>("CcmSuggestedPriceNote")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)");
|
||||
|
||||
b.Property<Guid?>("ContractId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
@ -4680,6 +4684,10 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<string>("ProSuggestedPriceNote")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user