Files
qhmes/yy-admin-master/YY.Admin/ViewModels/RawMaterialEntry/RawMaterialEntryOperationViewModel.cs

528 lines
22 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Prism.Commands;
using System.Collections.ObjectModel;
using System.IO;
using System.Text.Json;
using YY.Admin.Core.Entity;
using YY.Admin.Core.Services;
using YY.Admin.Services.Service;
namespace YY.Admin.ViewModels.RawMaterialEntry;
/// <summary>
/// 「新增原料入场记录」独立页面:左侧表单逻辑继承编辑 VM右侧展示当日入场简要列表并支持选中回填模板。
/// </summary>
public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogViewModel
{
private const int TodayListFetchSize = 5000;
private readonly IRawMaterialCardService _rawMaterialCardService;
private static readonly JsonSerializerOptions LayoutJsonOpts = new()
{
PropertyNameCaseInsensitive = true,
WriteIndented = true
};
private static string LayoutFilePath => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"YY.Admin",
"raw-material-entry-add-layout.json");
private bool _suppressTodaySelectionReaction;
public ObservableCollection<MesXslRawMaterialEntry> TodayEntries { get; } = new();
public IReadOnlyList<string> DateRangeOptions { get; } =
["今日", "过去24小时", "过去48小时", "过去72小时"];
private string _selectedDateRange = "今日";
public string SelectedDateRange
{
get => _selectedDateRange;
set
{
if (SetProperty(ref _selectedDateRange, value))
_ = LoadTodayEntriesAsync();
}
}
private MesXslRawMaterialEntry? _selectedTodayEntry;
public MesXslRawMaterialEntry? SelectedTodayEntry
{
get => _selectedTodayEntry;
set
{
if (!SetProperty(ref _selectedTodayEntry, value)) return;
if (_suppressTodaySelectionReaction || value == null) return;
_ = ApplyTodayRowToFormAsync(value);
}
}
private bool _isRightPanelExpanded = true;
public bool IsRightPanelExpanded
{
get => _isRightPanelExpanded;
set
{
if (!SetProperty(ref _isRightPanelExpanded, value)) return;
SaveLayoutState();
}
}
private double _expandedRightPanelWidth = 280;
/// <summary>右侧面板展开时的目标宽度(像素),由 GridSplitter 拖拽结束时回写。</summary>
public double ExpandedRightPanelWidth
{
get => _expandedRightPanelWidth;
private set
{
var v = Math.Clamp(value, 200, 560);
if (SetProperty(ref _expandedRightPanelWidth, v))
SaveLayoutState();
}
}
/// <summary>
/// 「生成原材料卡片」按钮可用条件:
/// 入场记录已保存(有 Id且至少存在一行「未生成卡片 + 份数>0」的明细。
/// 已打印态下用户「继续拆码」新增了行,按钮自动重新可用;全部行都已生成卡片时不可用。
/// </summary>
public bool CanGenerateCards =>
!string.IsNullOrWhiteSpace(Entry?.Id)
&& SplitCodeDetails.Any(d => !d.HasCard && (d.Portions ?? 0) > 0);
public DelegateCommand ToggleRightPanelCommand { get; }
public DelegateCommand RefreshTodayEntriesCommand { get; }
public DelegateCommand GenerateRawMaterialCardsCommand { get; }
/// <summary>「重新拆码」:清除已生成的卡片 + 清空明细,仅在编辑态可用。</summary>
public DelegateCommand ResplitCommand { get; }
public RawMaterialEntryOperationViewModel(
IRawMaterialEntryService entryService,
IJeecgDictSyncService dictSyncService,
IMixerMaterialService mixerMaterialService,
IRawMaterialCardService rawMaterialCardService,
IContainerExtension container,
IRegionManager regionManager)
: base(entryService, dictSyncService, mixerMaterialService, container, regionManager)
{
_rawMaterialCardService = rawMaterialCardService;
LoadLayoutState();
ToggleRightPanelCommand = new DelegateCommand(() => IsRightPanelExpanded = !IsRightPanelExpanded);
RefreshTodayEntriesCommand = new DelegateCommand(async () => await LoadTodayEntriesAsync());
GenerateRawMaterialCardsCommand = new DelegateCommand(async () => await GenerateRawMaterialCardsAsync());
ResplitCommand = new DelegateCommand(async () => await ResplitAsync(), () => CanResplit)
.ObservesProperty(() => Entry);
// 集合变化:批量重订阅 item.PropertyChanged 监听 HasCard/Portions并同步刷新两个 Can*。
SplitCodeDetails.CollectionChanged += OnSplitCodeDetailsCollectionChangedForCanFlags;
}
/// <summary>
/// 「重新拆码」按钮可用条件:入场记录已保存 + 至少有一行已生成卡片。
/// 全是新增未生成的行时,用户直接点「删除」即可,无需走「清空卡片」流程。
/// </summary>
public bool CanResplit =>
!string.IsNullOrWhiteSpace(Entry?.Id)
&& SplitCodeDetails.Any(d => d.HasCard);
/// <summary>
/// 集合变化时:对新增/移除的 item 维护 PropertyChanged 订阅,并刷新两个 Can* 属性。
/// HasCard / Portions 变化会让「生成原材料卡片」和「重新拆码」按钮可用性同步刷新。
/// </summary>
private void OnSplitCodeDetailsCollectionChangedForCanFlags(
object? sender,
System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.OldItems != null)
{
foreach (RawMaterialSplitDetailItem o in e.OldItems)
o.PropertyChanged -= OnSplitDetailHasCardChanged;
}
if (e.NewItems != null)
{
foreach (RawMaterialSplitDetailItem n in e.NewItems)
n.PropertyChanged += OnSplitDetailHasCardChanged;
}
RaisePropertyChanged(nameof(CanGenerateCards));
RaisePropertyChanged(nameof(CanResplit));
ResplitCommand.RaiseCanExecuteChanged();
}
private void OnSplitDetailHasCardChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName is nameof(RawMaterialSplitDetailItem.HasCard)
or nameof(RawMaterialSplitDetailItem.Portions))
{
RaisePropertyChanged(nameof(CanGenerateCards));
RaisePropertyChanged(nameof(CanResplit));
ResplitCommand.RaiseCanExecuteChanged();
}
}
public override void InitializeForAdd()
{
_suppressTodaySelectionReaction = true;
_selectedTodayEntry = null;
RaisePropertyChanged(nameof(SelectedTodayEntry));
_suppressTodaySelectionReaction = false;
base.InitializeForAdd();
RaisePropertyChanged(nameof(CanGenerateCards));
}
/// <summary>页面首次加载时拉取「今日」列表(由 View Loaded 调用)。</summary>
public async Task LoadTodayEntriesOnFirstShowAsync() => await LoadTodayEntriesAsync();
/// <summary>用户在拖拽结束 GridSplitter 后提交实际列宽。</summary>
public void CommitRightPanelWidthFromView(double actualWidth)
{
if (!IsRightPanelExpanded || actualWidth < 1) return;
ExpandedRightPanelWidth = actualWidth;
}
private async Task LoadTodayEntriesAsync()
{
try
{
IsLoading = true;
var result = await EntryService.PageAsync(1, TodayListFetchSize);
DateTime? threshold = _selectedDateRange switch
{
"过去24小时" => DateTime.Now.AddHours(-24),
"过去48小时" => DateTime.Now.AddHours(-48),
"过去72小时" => DateTime.Now.AddHours(-72),
_ => null // "今日":按自然日过滤
};
var rows = result.Records
.Where(e => IsInRange(e, threshold))
.OrderByDescending(e => e.EntryTime ?? e.CreateTime ?? DateTime.MinValue)
.ToList();
TodayEntries.Clear();
foreach (var r in rows)
TodayEntries.Add(r);
}
catch
{
// 列表失败不阻断左侧新增(与物料加载策略一致)
}
finally
{
IsLoading = false;
}
}
private static bool IsInRange(MesXslRawMaterialEntry e, DateTime? threshold)
{
if (threshold == null)
{
var today = DateTime.Today;
return (e.EntryTime?.Date == today) || (e.CreateTime?.Date == today);
}
return (e.EntryTime >= threshold) || (e.CreateTime >= threshold);
}
/// <summary>
/// 点击右侧今日记录:直接以「编辑模式」加载源记录到左侧表单(保留 Id/条码/批次号),
/// 不再走「新增模板」逻辑,避免重新生成条码、覆盖原数据。
/// </summary>
private async Task ApplyTodayRowToFormAsync(MesXslRawMaterialEntry src)
{
// 复用基类 InitializeForEdit保留 Id/Barcode/BatchNo/状态等所有字段,标题自动切到「编辑原料入场记录」
base.InitializeForEdit(src);
RaisePropertyChanged(nameof(CanGenerateCards));
// 若物料列表此前未加载导致选中项未回填,则补一次拉取(与编辑弹窗逻辑一致)
if (_pendingMaterialId != null)
await LoadMaterialOptionsAsync();
}
/// <summary>
/// 保存成功后:无论新增还是编辑,都立即刷新右侧「今日入场」列表。
/// 编辑成功额外把表单切回「新增态」,避免用户连续误改同一条;新增成功后由基类负责清空表单。
/// </summary>
protected override async Task SaveAsync()
{
var wasEdit = !IsAddMode;
await base.SaveAsync();
RaisePropertyChanged(nameof(CanGenerateCards));
if (!Result || CloseAction != null)
{
return;
}
await LoadTodayEntriesAsync();
if (wasEdit)
{
HandyControl.Controls.MessageBox.Success("编辑成功!");
InitializeForAdd();
}
}
/// <summary>
/// 「重新拆码」:弹窗预提示「将清除 N 张原材料卡片」→ 确认 → 后端按 splitDetailId IN 批删
/// → 清空 SplitCodeDetails → Entry.PrintFlag=0 → EditAsync(Entry) 持久化新状态 → 刷新今日列表。
/// 流程对应「清空卡片重新生成」的业务诉求;用户可立即重新维护明细并再次「生成原材料卡片」。
/// </summary>
private async Task ResplitAsync()
{
if (Entry == null || string.IsNullOrWhiteSpace(Entry.Id))
{
HandyControl.Controls.MessageBox.Warning("请先选中一条已保存的入场记录后再「重新拆码」。");
return;
}
var detailIds = SplitCodeDetails
.Select(d => d.Id)
.Where(id => !string.IsNullOrWhiteSpace(id))
.Distinct()
.ToList();
try
{
IsLoading = true;
// dryRun 预查:统计当前明细行已生成的卡片数量
var cardCount = detailIds.Count > 0
? await _rawMaterialCardService.DeleteBySplitDetailIdsAsync(detailIds, dryRun: true)
: 0;
if (cardCount < 0)
{
HandyControl.Controls.MessageBox.Error("无法连接服务器,重新拆码已取消。");
return;
}
var confirm = HandyControl.Controls.MessageBox.Show(
$"当前入场记录已生成 {cardCount} 条原材料卡片,是否清空卡片重新生成?",
"重新拆码",
System.Windows.MessageBoxButton.OKCancel,
System.Windows.MessageBoxImage.Question);
if (confirm != System.Windows.MessageBoxResult.OK)
{
return;
}
// 真正执行批删(即使 cardCount=0 也调用一次,结果幂等)
if (cardCount > 0)
{
var deleted = await _rawMaterialCardService.DeleteBySplitDetailIdsAsync(detailIds, dryRun: false);
if (deleted < 0)
{
HandyControl.Controls.MessageBox.Error("清除已生成的原材料卡片失败,重新拆码已中止。");
return;
}
}
// 清空明细 + 重置打印标记,并把变更持久化到后端入场记录
SplitCodeDetails.Clear();
Entry.PrintFlag = "0";
ApplySplitDetailsToEntry(); // 同步把 PortionDetailIds 等清空到 Entry 上
var ok = await EntryService.EditAsync(Entry);
if (!ok)
{
HandyControl.Controls.MessageBox.Error("保存重新拆码后的入场记录失败,请刷新后重试。");
return;
}
RaisePropertyChanged(nameof(Entry));
RaisePropertyChanged(nameof(IsPrinted));
RaisePropertyChanged(nameof(IsTotalWeightEditable));
AddSplitDetailCommand.RaiseCanExecuteChanged();
RaisePropertyChanged(nameof(CanGenerateCards));
RaisePropertyChanged(nameof(CanResplit));
HandyControl.Controls.MessageBox.Success($"已清除 {cardCount} 条原材料卡片,请重新维护拆码明细。");
await LoadTodayEntriesAsync();
}
catch (Exception ex)
{
HandyControl.Controls.MessageBox.Error($"重新拆码失败:{ex.Message}");
}
finally
{
IsLoading = false;
}
}
/// <summary>
/// 「生成原材料卡片」:仅处理 HasCard==false 的明细行(「继续拆码」流程的核心)。
/// 条码续号 = 已有卡片总数(Σ HasCard==true 行的 Portions+ 1避免与已生成卡片的条码冲突。
/// 成功生成的行置 HasCard=trueEntry.PrintFlag=1整票级标记
/// </summary>
private async Task GenerateRawMaterialCardsAsync()
{
if (Entry == null || string.IsNullOrWhiteSpace(Entry.Id))
{
HandyControl.Controls.MessageBox.Warning("请先保存入场记录再生成原材料卡片!");
return;
}
// 只处理未生成卡片的行 + 份数>0已生成行直接跳过避免重复加卡和条码冲突
var pendingRows = SplitCodeDetails
.Where(d => !d.HasCard && (d.Portions ?? 0) > 0)
.ToList();
if (pendingRows.Count == 0)
{
HandyControl.Controls.MessageBox.Warning("当前没有待生成卡片的拆码明细(已生成的行不会重复处理)。请先「新增明细」再生成。");
return;
}
// 库位必填仅校验本批待生成行;行号取在原集合的真实索引,避免编号偏移误导
foreach (var row in pendingRows)
{
if (string.IsNullOrWhiteSpace(row.WarehouseLocation))
{
var realIndex = SplitCodeDetails.IndexOf(row);
HandyControl.Controls.MessageBox.Warning($"拆码明细第 {realIndex + 1} 行的「库位」未填写,请点击该行库位选择库区后再生成原材料卡片。");
return;
}
}
// 总重核对:拆码明细合计 vs 基础资料总重
{
var splitTotal = SplitCodeDetails.Sum(d => (d.PortionWeight ?? 0d) * (d.Portions ?? 0));
var basicTotal = Entry.TotalWeight ?? 0d;
if (Math.Abs(splitTotal - basicTotal) > 0.01d)
{
string hint;
if (splitTotal > basicTotal)
hint = $"本次打印拆码总重 {splitTotal:0.##},超出基础资料总重 {basicTotal:0.##},超出部分是否需要核对?";
else
hint = $"本次打印拆码总重 {splitTotal:0.##},少于基础资料总重 {basicTotal:0.##},剩余部分将无法继续拆码,是否继续?";
var confirm = System.Windows.MessageBox.Show(
hint,
"总重核对",
System.Windows.MessageBoxButton.OKCancel,
System.Windows.MessageBoxImage.Warning);
if (confirm != System.Windows.MessageBoxResult.OK) return;
}
}
try
{
IsLoading = true;
// 续号起点:已有卡片数 = Σ HasCard==true 行的 Portions新增卡片从其后续编
var alreadyGenerated = SplitCodeDetails
.Where(d => d.HasCard)
.Sum(d => d.Portions ?? 0);
var globalIndex = alreadyGenerated + 1;
var baseBarcode = Entry.Barcode ?? "";
var failCount = 0;
var newCardCount = 0;
// 按行收集成功标记:只要该行所有份都加卡成功,行 HasCard=true
// 中途任一份失败则保留 HasCard=false下次再点「生成」时会重试该行
var rowSuccessMap = new Dictionary<RawMaterialSplitDetailItem, bool>();
foreach (var detail in pendingRows)
{
var portions = detail.Portions ?? 0;
var rowAllOk = true;
for (var i = 0; i < portions; i++)
{
var card = new MesXslRawMaterialCard
{
// 关联到当前拆码明细行 — 便于「重新拆码」按此 ID 批删
SplitDetailId = detail.Id,
Barcode = baseBarcode + globalIndex.ToString("D3"),
BatchNo = Entry.BatchNo,
EntryDate = Entry.EntryTime?.Date ?? DateTime.Today,
MaterialId = Entry.MaterialId,
MaterialName = Entry.MaterialName,
SupplierId = Entry.SupplierId,
SupplierName = Entry.SupplierName,
ManufacturerMaterialName = Entry.ManufacturerMaterialName,
ShelfLife = Entry.ShelfLife,
TotalWeight = detail.PortionWeight.HasValue ? (decimal?)detail.PortionWeight.Value : null,
RemainingWeight = detail.PortionWeight.HasValue ? (decimal?)detail.PortionWeight.Value : null,
RemainingQuantity = detail.PortionPackages,
WarehouseArea = detail.WarehouseLocation,
UnloadOperator = Entry.UnloadOperator,
Status = "1",
TestResult = "0",
PriorityPickup = "0",
TenantId = Entry.TenantId
};
var ok = await _rawMaterialCardService.AddAsync(card);
if (ok) newCardCount++;
else { failCount++; rowAllOk = false; }
globalIndex++;
}
rowSuccessMap[detail] = rowAllOk;
}
// 把整行加卡都成功的标记 HasCard=true触发该行 UI 自动锁定 + 删除按钮隐藏 + 打印列显示「已打印」)
foreach (var kv in rowSuccessMap)
{
if (kv.Value) kv.Key.HasCard = true;
}
// 整票级 PrintFlag只要本批至少有一张卡片成功即置 1与原行为兼容
// 关键:必须先 ApplySplitDetailsToEntry(),把刚被设为 true 的 HasCard 序列化到 PortionCardFlags
// 否则下次重新打开时该行会回退为 falsePortionCardFlags 旧值未更新),从而被「继续拆码」流程误纳入待生成。
if (newCardCount > 0)
{
Entry.PrintFlag = "1";
ApplySplitDetailsToEntry();
await EntryService.EditAsync(Entry);
RaisePropertyChanged(nameof(Entry));
RaisePropertyChanged(nameof(IsPrinted));
RaisePropertyChanged(nameof(IsTotalWeightEditable));
AddSplitDetailCommand.RaiseCanExecuteChanged();
}
RaisePropertyChanged(nameof(CanGenerateCards));
RaisePropertyChanged(nameof(CanResplit));
if (failCount == 0)
HandyControl.Controls.MessageBox.Success($"已生成 {newCardCount} 张原材料卡片(累计 {alreadyGenerated + newCardCount} 张),打印状态已更新为「已打印」!");
else
HandyControl.Controls.MessageBox.Warning($"本次共尝试生成 {newCardCount + failCount} 张,成功 {newCardCount} 张,失败 {failCount} 张。失败的行未标记为「已打印」,可检查网络后再次点击「生成原材料卡片」重试。");
}
catch (Exception ex)
{
HandyControl.Controls.MessageBox.Error($"生成原材料卡片失败:{ex.Message}");
}
finally
{
IsLoading = false;
}
}
private void LoadLayoutState()
{
try
{
if (!File.Exists(LayoutFilePath)) return;
var json = File.ReadAllText(LayoutFilePath);
var dto = JsonSerializer.Deserialize<AddPageLayoutDto>(json, LayoutJsonOpts);
if (dto == null) return;
if (dto.ExpandedWidth is > 0 and < 2000)
_expandedRightPanelWidth = Math.Clamp(dto.ExpandedWidth.Value, 200, 560);
if (dto.IsExpanded.HasValue)
_isRightPanelExpanded = dto.IsExpanded.Value;
RaisePropertyChanged(nameof(ExpandedRightPanelWidth));
RaisePropertyChanged(nameof(IsRightPanelExpanded));
}
catch { /* 布局文件损坏时忽略 */ }
}
private void SaveLayoutState()
{
try
{
var dir = Path.GetDirectoryName(LayoutFilePath);
if (!string.IsNullOrEmpty(dir))
Directory.CreateDirectory(dir);
var dto = new AddPageLayoutDto
{
ExpandedWidth = _expandedRightPanelWidth,
IsExpanded = _isRightPanelExpanded
};
File.WriteAllText(LayoutFilePath, JsonSerializer.Serialize(dto, LayoutJsonOpts));
}
catch { /* 写本地失败时不影响业务 */ }
}
private sealed class AddPageLayoutDto
{
public double? ExpandedWidth { get; set; }
public bool? IsExpanded { get; set; }
}
}