using EgwCoreLib.Lux.Core; using EgwCoreLib.Lux.Core.RestPayload; using EgwCoreLib.Lux.Data; using EgwCoreLib.Lux.Data.DbModel.Config; using EgwCoreLib.Lux.Data.DbModel.Sales; using EgwCoreLib.Lux.Data.DbModel.Utils; using EgwCoreLib.Lux.Data.Services; using EgwMultiEngineManager.Data; using Lux.UI.Components.Compo.Config; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Forms; using Microsoft.EntityFrameworkCore.Storage.Json; using Microsoft.JSInterop; using NLog.LayoutRenderers; using System.Text; using WebWindowComplex; using WebWindowComplex.DTO; using static EgwCoreLib.Lux.Core.Enums; namespace Lux.UI.Components.Compo { public partial class OfferRowMan : IDisposable { #region Public Enums /// /// modalità modifica riga offerta /// public enum EditMode { None = 0, /// /// Dati generici del record /// RecData, /// /// Struttura serializzata (es JWD) /// SerStruc, /// /// BOM editing /// BOM, /// /// File editing (es BTL) /// File } #endregion Public Enums #region Public Properties [Parameter] public OfferModel CurrRecord { get; set; } = null!; [Parameter] public DisplayMode DisplayMode { get; set; } = DisplayMode.Standard; [Parameter] public EventCallback EC_Updated { get; set; } #endregion Public Properties #region Public Methods /// /// Dispose sottoscrizione canale /// public void Dispose() { DLService.UpdatePipe.EA_NewMessage -= UpdatePipe_EA_NewMessage; DLService.CalcDonePipe.EA_NewMessage -= CalcDonePipe_EA_NewMessage; } #endregion Public Methods #region Protected Fields /// /// Predisposizione valori live SVG/JWD /// protected LivePayload CurrData = new LivePayload(); /// /// Configurazione elenchi anagrafiche /// protected BaseListPayload SetupList = new BaseListPayload(); #endregion Protected Fields #region Protected Properties [Inject] protected ConfigDataService CDService { get; set; } = null!; [Inject] protected IConfiguration Config { get; set; } = null!; [Inject] protected CalcRequestService CService { get; set; } = null!; [Inject] protected DataLayerServices DLService { get; set; } = null!; /// /// Costo totale calcolato x offerta /// protected double GrandTotCost { get => AllRecords != null && AllRecords.Count > 0 ? AllRecords.Sum(x => x.TotalCost) : 0; } /// /// Margine medio calcolato x offerta /// protected double GrandTotMargin { get { double answ = 0; if (AllRecords != null && AllRecords.Count > 0) { double totPrice = AllRecords.Sum(x => x.TotalPrice); double totCost = AllRecords.Sum(x => x.TotalCost); if (totPrice > 0) { answ = (totPrice - totCost) / totPrice; } } return answ; } } /// /// Importo totale calcolato x offerta /// protected double GrandTotPrice { get => AllRecords != null && AllRecords.Count > 0 ? AllRecords.Sum(x => x.TotalPrice) : 0; } /// /// Num totale obj calcolato x offerta /// protected double GrandTotQty { get => AllRecords != null && AllRecords.Count > 0 ? AllRecords.Sum(x => x.Qty) : 0; } [Inject] protected IWebHostEnvironment HostEnv { get; set; } = null!; [Inject] protected ImageCacheService ICService { get; set; } = null!; [Inject] protected IJSRuntime JSRuntime { get; set; } = null!; /// /// ID Offerta corrente /// protected int OfferID { get => CurrRecord.OfferID; } #endregion Protected Properties #region Protected Methods protected void ClosePopup() { CurrEditMode = EditMode.None; EditRecord = null; } /// /// Annullamento modifica /// /// /// protected async Task DoCancel() { isLoading = true; CurrEditMode = EditMode.None; EditRecord = null; await Task.Delay(20); await DLService.FlushCacheOffersAsync(); await Task.Delay(20); await ReloadData(); UpdateTable(); isLoading = false; } /// /// Clona riga richiesta /// /// protected async Task DoClone(OfferRowModel rec2clone) { if (!await JSRuntime.InvokeAsync("confirm", $"Confermi di voler duplicare la riga corrente?")) return; // calcolo indice riga... int numRow = totalCount + 1; OfferRowModel newRec = new OfferRowModel() { AwaitBom = true, AwaitPrice = true, Envir = rec2clone.Envir, FileName = rec2clone.FileName, FileResource = rec2clone.FileResource, FileSize = rec2clone.FileSize, Inserted = DateTime.Now, ItemBOM = rec2clone.ItemBOM, ItemSteps = rec2clone.ItemSteps, Modified = DateTime.Now, Note = rec2clone.Note, OfferID = OfferID, OfferRowUID = "", Qty = rec2clone.Qty, RowNum = numRow, SellingItemID = rec2clone.SellingItemID, SerStruct = rec2clone.SerStruct, StepCost = rec2clone.StepCost, StepPrice = rec2clone.StepPrice, }; // salvo sul DB await DLService.OffertRowUpsert(newRec); // chiamo update record che non hanno UID x questo ordine var list2fix = await DLService.OffertRowFixUid(OfferID); if (list2fix != null && list2fix.Count > 0) { // rileggo i miei record... await ReloadData(); var listCalc = SorListCalc(); foreach (var item in listCalc) { // se UID è tra quelli da ricalcolare... if (list2fix.Contains(item.OfferRowUID)) { // chiedo BOM e immagine await reqBomUpdate(item); } } } await ReloadData(); UpdateTable(); } /// /// Eliminazione riga offerta /// /// /// protected async Task DoDelete(OfferRowModel rec2del) { if (!await JSRuntime.InvokeAsync("confirm", $"Confermi di voler eliminare la riga corrente?
Codice: {rec2del.OfferRowUID} | {rec2del.Note} | importo tot: {rec2del.TotalPrice}")) return; await DLService.OffertRowDelete(rec2del); await ReloadData(); UpdateTable(); } /// /// Va in edit della riga richiesta /// /// protected void DoEdit(OfferRowModel curRec) { #if false // preparazione dati da record corrente PrepareWindowData(curRec.SerStruct); // reset prev prevJwd = ""; #endif // imposto edit record EditRecord = curRec; /// modalitàedit: gestione valori campi record CurrEditMode = EditMode.RecData; } /// /// Apre editor finestre del record richiesto /// /// protected void DoEditJwd(OfferRowModel curRec) { EditRecord = curRec; /// modalitàedit: gestione JWD CurrEditMode = EditMode.SerStruc; // preparazione dati da record corrente PrepareWindowData(EditRecord.SerStruct); // reset prev prevJwd = ""; } /// /// Salvataggio edit record + reload /// /// /// protected async Task DoSave(OfferRowModel curRec) { isLoading = true; // salvo record modificato... await DLService.OffertRowUpsert(curRec); // reset CurrEditMode = EditMode.None; EditRecord = null; await DLService.FlushCacheOffersAsync(); await ReloadData(); UpdateTable(); isLoading = false; } protected void DoSwapMat(OfferRowModel currRow) { CurrEditMode = EditMode.BOM; EditRecord = currRow; CurrBomList = DLService.OffertGetBomList(EditRecord); } /// /// Display fileSize scalato /// /// /// protected string fSize(long size) { return EgwCoreLib.Utils.FileHelpers.SizeSuffix(size, 1); } /// /// Calcolo URL immagine /// /// /// /// protected string imgUrl(string imgUid, string env) { // cast string su env.. EgwMultiEngineManager.Data.Constants.EXECENVIRONMENTS envir = EgwMultiEngineManager.Data.Constants.EXECENVIRONMENTS.WINDOW; Enum.TryParse(env, out envir); return ICService.ImageUrl($"{apiUrl}/{imgBasePath}", false, imgUid, envir); } /// /// Forza parametri general selezionati nell'offerta /// /// protected async Task OfferForceParameters() { if (!await JSRuntime.InvokeAsync("confirm", $"Confermi di voler impostare i parametri selezionati per l'offerta?")) return; // recupero obj dizionario x i parametri compresi... ParamDict CurrSel = new ParamDict(CurrRecord.DictPresel); // metto a waiting tutte le righe con bom... var listCalc = SorListCalc(); foreach (var item in listCalc) { await DLService.OffertUpdateAwaitState(item.OfferRowID, true, true); // poiché non è gestito evento ritorno update window interno si "scassa" --> try catch/ if FALSE try { string rColor = CurrSel.GetVal("Color"); string rGlass = CurrSel.GetVal("Glass"); string rProfile = CurrSel.GetVal("Profile"); string rWood = CurrSel.GetVal("Wood"); var newSerStruct = SerialMan.MassUpdate((string)item.SerStruct, null, null, rColor, rWood, rGlass, rProfile); await DLService.OffertRowUpdateSerStruct(item.OfferRowID, newSerStruct); } catch { } } await InvokeAsync(StateHasChanged); // verifica preliminare UID await DLService.OffertRowFixUid(OfferID); // ricalcolo di tutte le BOM e relativi prezzi... foreach (var item in listCalc) { // chiedo BOM e immagine await reqBomUpdate(item); } //await DoRecalcOffer(); } /// /// Aggiornamento costing completo: /// - verifica UID /// - ricalcolo BOM /// - update prezzi /// /// protected async Task OfferUpdateAllCosting() { if (!await JSRuntime.InvokeAsync("confirm", $"Confermi di voler ricalcolare/validare in toto l'offerta?")) return; // metto a waiting tutte le righe con bom... var listCalc = SorListCalc(); foreach (var item in listCalc) { await DLService.OffertUpdateAwaitState(item.OfferRowID, true, true); } await InvokeAsync(StateHasChanged); // verifica preliminare UID await DLService.OffertRowFixUid(OfferID); // fixme todo da riverificare con calcolo BOM funzionante #if false // rileggo i record... await ReloadData(); #endif // ricalcolo di tutte le BOM e relativi prezzi... foreach (var item in listCalc) { // chiedo BOM e immagine await reqBomUpdate(item); } #if false await DoRecalcOffer(); #endif } /// /// Verifica e ricalcolo dei prezzi degli items nell'offerta /// /// protected async Task OfferUpdatePrices() { if (!await JSRuntime.InvokeAsync("confirm", $"Confermi di voler ricalcolare a costi correnti offerta?")) return; await DoRecalcOffer(); } /// /// init obj /// protected override async Task OnInitializedAsync() { ConfInit(); prevJwd = ""; await ReloadBaseList(); DLService.UpdatePipe.EA_NewMessage += UpdatePipe_EA_NewMessage; DLService.CalcDonePipe.EA_NewMessage += CalcDonePipe_EA_NewMessage; } protected override async Task OnParametersSetAsync() { await ReloadData(); UpdateTable(); } /// /// Lancia la richiesta di ricaolo della BOM dal JWD (o equivalente) /// /// protected async Task RequestBom(OfferRowModel currRec) { if (!await JSRuntime.InvokeAsync("confirm", $"Confermi di voler ricalcolare la BOM?")) return; await reqBomUpdate(currRec); } /// /// Css di verifica riga selezionata /// /// /// protected string RowClass(OfferRowModel selRow) { return EditRecord != null && EditRecord.OfferRowID == selRow.OfferRowID ? "table-info" : ""; } #endregion Protected Methods #region Private Fields private List AllColors = new(); private List AllConfEnvir = new(); private List AllConfGlass = new(); private List AllConfHardware = new(); private List AllConfProfile = new(); private List AllConfWood = new(); private List AllRecords = new List(); private string apiUrl = ""; private List AvailColorMaterialList = new List(); private List AvailFamilyHardwareList = new List(); private List AvailGlassList = new List(); private List AvailHardwareList = new(); private List AvailMaterialList = new List(); private List AvailProfileList = new List(); private string calcTag = "calc"; private List? CurrBomList = null; /// /// Modalità editint attiva /// private EditMode CurrEditMode = EditMode.None; private int currPage = 1; private string currSvg = ""; /// /// Record in Edit corrente /// private OfferRowModel? EditRecord = null; private string genBasePath = "generic"; private string GenericBasePath = ""; private string imgBasePath = ""; private bool isLoading = false; private List ListRecords = new List(); private int numRecord = 10; /// /// Versione originale (pre edit) /// private string origJwd = ""; /// /// Versione precedente JWD x test e confronto /// private string prevJwd = ""; /// /// Dizionario richieste /// private Dictionary reqDict = new Dictionary(); private string subChannel = ""; private int totalCount = 0; #endregion Private Fields #region Private Methods /// /// Ricevuto SVG, se è il mio lo aggiorno... /// /// /// private async void CalcDonePipe_EA_NewMessage(object? sender, EventArgs e) { // vale SOLO SE sono in editing... if (EditRecord != null) { // aggiorno visualizzazione PubSubEventArgs currArgs = (PubSubEventArgs)e; // conversione on-the-fly SVG da mostrare if (!string.IsNullOrEmpty(currArgs.newMessage)) { if (currArgs.msgUid.Equals($"{subChannel}:{EditRecord.OfferRowUID}")) { currSvg = currArgs.newMessage; // salvo in live data... CurrData.SvgPreview = currSvg; } await InvokeAsync(StateHasChanged); } } } /// /// Chiude edit andando eventualmente a salvare /// /// /// private async Task CloseEdit(bool doSave) { // ...se ho editing if (EditRecord != null) { bool updateBom = false; // SE richiesto salvataggio... if (doSave) { // salvo su DB! await DLService.OffertRowUpdateSerStruct(EditRecord.OfferRowID, prevJwd); updateBom = true; } else // altrimenti ricalcolo valore salvato { prevJwd = EditRecord.SerStruct; CurrData.CurrJwd = EditRecord.SerStruct; } if (updateBom) { await reqBomUpdate(EditRecord); } // aggiorno nel dizionari if (reqDict.ContainsKey("Jwd")) { reqDict["Jwd"] = prevJwd; } if (reqDict != null && reqDict.Count > 0) { // chiamo richiesta update CalcRequestDTO calcRequestDTO = new CalcRequestDTO(); calcRequestDTO.EnvType = EditRecord.Envir; calcRequestDTO.DictExec = reqDict; // chiamo la chiamata POST alla API, che manda la richiesta via REDIS await ICService.CallRestPost($"{apiUrl}/{GenericBasePath}", $"{calcTag}/{EditRecord.OfferRowUID}", calcRequestDTO); } EditRecord = null; CurrEditMode = EditMode.None; } } private void ConfInit() { apiUrl = Config.GetValue("ServerConf:Prog.ApiUrl") ?? ""; imgBasePath = Config.GetValue("ServerConf:ImageBaseUrl") ?? ""; GenericBasePath = Config.GetValue("ServerConf:GenericBaseUrl") ?? ""; calcTag = Config.GetValue("ServerConf:CalcTag") ?? "calc"; subChannel = Config.GetValue("ServerConf:SvgChannel") ?? ""; } private async Task DoRecalcOffer() { isLoading = true; // ciclo sulle righe... await setAwaitPrice(true, false); await Task.Delay(50); await DLService.OffertUpdateCost(OfferID); await Task.Delay(50); await setAwaitPrice(false, true); await ReloadData(); UpdateTable(); isLoading = false; await EC_Updated.InvokeAsync(true); } /// /// Svuota cache corrente + rilegge dati /// private async Task ForceReloadData() { isLoading = true; CurrEditMode = EditMode.None; EditRecord = null; await Task.Delay(20); await DLService.FlushCacheOffersAsync(); await Task.Delay(20); await ReloadData(); UpdateTable(); isLoading = false; } /// /// Preparazione dati x componente edit JWD /// /// private void PrepareWindowData(string currJwd) { // preparo conf oggetti x controllo SetupList = SetupList = new BaseListPayload() { ColorMaterial = AvailColorMaterialList, FamilyHardware = AvailFamilyHardwareList, Glass = AvailGlassList, Hardware = AvailHardwareList, Material = AvailMaterialList, Profile = AvailProfileList, TemplateDTO = null }; CurrData = new LivePayload() { CurrJwd = currJwd, SvgPreview = currSvg }; } /// /// init classi configurazione /// /// private async Task ReloadBaseList() { // lettura config setup varie da DB/Cache Redis AllConfEnvir = await DLService.ConfEnvirParamGetAllAsync(); AllConfGlass = await DLService.ConfGlassGetAllAsync(); var rawHw = CDService.ElencoHw(EgwMultiEngineManager.Data.Constants.EXECENVIRONMENTS.WINDOW, "HW.AGB"); // hw filtro solo validi... AllConfHardware = rawHw .Where(x => !x.FamilyName.Equals(x.Description, StringComparison.OrdinalIgnoreCase)) .ToList(); AllConfProfile = await DLService.ConfProfileGetAllAsync(); AllConfWood = await DLService.ConfWoodGetAllAsync(); AllColors = await DLService.GenValGetFiltAsync("WoodCol"); // conversione tipi AvailGlassList = AllConfGlass .Select(x => x.Description) .ToList(); AvailProfileList = AllConfProfile .Select(x => x.Description) .ToList(); AvailFamilyHardwareList = AllConfHardware .DistinctBy(x => x.FamilyName) .OrderBy(x => x.FamilyName) .Select(x => x.FamilyName) .ToList(); AvailHardwareList = AllConfHardware .Select(x => new Egw.Window.Data.Hardware(x.Id, x.FamilyName, x.Description, x.OpeningType, x.Shape, x.SashQty, x.SashPosition)) .ToList(); AvailMaterialList = AllConfWood .Select(x => x.Description) .ToList(); // FixMe Todo: aggiunta profili (manca anche nel costruttore...) AvailColorMaterialList = AllColors .OrderBy(x => x.Ordinal) .Select(x => x.ValString) .ToList(); } /// /// Legge i dati dei record completi /// private async Task ReloadData() { if (OfferID > 0) { AllRecords = await DLService.OfferRowGetByOffer(OfferID); totalCount = AllRecords.Count(); } } /// /// Effettua vera richiesta della BOM /// /// /// private async Task reqBomUpdate(OfferRowModel currRec) { // salvo richiesta BOM su record currRec.AwaitBom = true; currRec.AwaitPrice = true; await DLService.OffertUpdateAwaitState(currRec.OfferRowID, true, true); // preparo la domanda serializzata Dictionary DictExec = new Dictionary(); // verifico parametri da conf envir... var envRec = AllConfEnvir.FirstOrDefault(x => x.EnvirID == currRec.Envir); string serKey = envRec != null ? envRec.SerStrucKey : "Jwd"; // cablata la BOM DictExec.Add("Mode", $"{(int)Egw.Window.Data.Enums.QuestionModes.BOM}"); //DictExec.Add("Mode", "2"); // UID cablato x ora... DictExec.Add("UID", currRec.OfferRowUID); DictExec.Add(serKey, currRec.SerStruct); CalcRequestDTO req = new CalcRequestDTO() { EnvType = currRec.Envir, //EnvType = currEnv, DictExec = DictExec }; await InvokeAsync(StateHasChanged); // chiamo la chiamata POST alla API, che manda la richiesta via REDIS await CService.CallRestPost($"{apiUrl}/{genBasePath}", $"{calcTag}/{currRec.OfferRowUID}", req); } /// /// Esegue salvataggio del file ricevuto /// /// Nome secure da impiegare /// Contenuto file private bool saveFileContent(string folderPath, string secureName, string content) { bool answ = false; if (!string.IsNullOrEmpty(folderPath)) { // calcolo path file... string filePath = Path.Combine("unsafe_uploads", folderPath, secureName); File.WriteAllText(filePath, content); } return answ; } /// /// Salvataggio del JWD aggiornato nella mia riga di offerta /// /// private async void SaveJWD(Dictionary args) { // ...se ho editing if (EditRecord != null) { // SE contiene il mio Jwd... if (args.ContainsKey("Jwd")) { reqDict = args; string serStruct = reqDict["Jwd"]; // controllo SE variato... if (!prevJwd.Equals(serStruct)) { // aggiorno val prev prevJwd = serStruct; // aggiorno live data CurrData.CurrJwd = serStruct; // chiamo richiesta update CalcRequestDTO calcRequestDTO = new CalcRequestDTO(); calcRequestDTO.EnvType = EditRecord.Envir; calcRequestDTO.DictExec = reqDict; // chiamo la chiamata POST alla API, che manda la richiesta via REDIS await ICService.CallRestPost($"{apiUrl}/{GenericBasePath}", $"{calcTag}/{EditRecord.OfferRowUID}", calcRequestDTO); #if false // salvo su DB! await DLService.OffertRowUpdateSerStruct(EditRecord.OfferRowID, serStruct); #endif } } } } private async Task setAwaitPrice(bool awaitPrice, bool flushCache) { foreach (var item in AllRecords) { await DLService.OffertUpdateAwaitState(item.OfferRowID, false, awaitPrice, flushCache); } } /// /// Elenco SalesOfferRows calcolabili: /// - contengono serializzazione come JWD /// - contengono file come BTL /// private List SorListCalc() { var rawList = AllRecords .Where(x => (!string.IsNullOrEmpty(x.SerStruct) && x.SerStruct.Length > 2) || (!string.IsNullOrEmpty(x.FileName) && !string.IsNullOrEmpty(x.FileResource))) .ToList(); return rawList ?? new List(); } /// /// Toggle visibilità modifica file indicando ID della OfferRow corrente (o zero se deselect) /// private void ToggleFileEdit(OfferRowModel? currRec) { CurrEditMode = currRec == null ? EditMode.None : EditMode.File; EditRecord = currRec; } /// /// Salva nel record corrente la BOM aggiornata e poi ricalcola importo... /// /// /// private async void UpdateBom(List newBomList) { if (EditRecord != null) { // salvo BOM nel record corrente... bool fatto = await DLService.OffertRowUpdateBom(EditRecord.OfferRowID, newBomList); // ricalcolo offerta completa await ReloadData(); UpdateTable(); } } /// /// Task verifica update ricevuti /// /// /// /// private async void UpdatePipe_EA_NewMessage(object? sender, EventArgs e) { // aggiorno visualizzazione PubSubEventArgs currArgs = (PubSubEventArgs)e; // conversione on-the-fly SVG da mostrare if (!string.IsNullOrEmpty(currArgs.newMessage)) { // cerco se faccia aprte dei record correnti... var recFound = AllRecords.Any(x => x.OfferRowUID == currArgs.newMessage); if (recFound) { isLoading = true; await Task.Delay(10); #if false if (currArgs.msgUid.Equals($"{subChannel}:{windowUid}")) { } #endif // se si tratta dell'UID corrente --> fa update await DoRecalcOffer(); await InvokeAsync(StateHasChanged); } } await Task.Delay(1); } /// /// Filtro e paginazione /// private void UpdateTable() { // fix paginazione ListRecords = AllRecords .OrderBy(x => x.RowNum) .Skip(numRecord * (currPage - 1)) .Take(numRecord) .ToList(); isLoading = false; } /// /// Esegue lettura file + invio richiesta specifica /// /// /// private async Task UploadFile(InputFileChangeEventArgs e) { if (EditRecord != null) { // init dizionari arg richiesta update Dictionary fileArgs = new Dictionary(); // leggo il contenuto del PRIMO (singolo) file IBrowserFile file = e.File; // limite file size (al momento 10 MB) var maxAllowedSize = 10 * 1024 * 1024; using var stream = file.OpenReadStream(maxAllowedSize); using var reader = new StreamReader(stream); string rawContent = await reader.ReadToEndAsync(); // calcolo il nome del file trusted... string trustedFileName = Path.GetRandomFileName(); EditRecord.FileResource = trustedFileName; EditRecord.FileName = file.Name; EditRecord.FileSize = rawContent.LongCount(); // salvo sul DB i dati (nome, nome sicuro, size...) await DLService.OffertRowUpdateFileData(EditRecord); // parametri richiesta fileArgs.Add("Mode", $"{(int)Egw.Window.Data.Enums.QuestionModes.PREVIEW}"); fileArgs.Add("Btl", rawContent); // invio! CalcRequestDTO calcRequestDTO = new CalcRequestDTO(); calcRequestDTO.EnvType = EgwMultiEngineManager.Data.Constants.EXECENVIRONMENTS.BEAM; calcRequestDTO.DictExec = fileArgs; await ICService.CallRestPost($"{apiUrl}/{GenericBasePath}", $"{calcTag}/{EditRecord.OfferRowUID}", calcRequestDTO); #if false // salvo in locale il file: SISTEMARE PERMESSI saveFileContent(EditFileRecord.OfferRowUID, trustedFileName, rawContent); #endif } } #endregion Private Methods } }