using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using AyaNova.Util; using AyaNova.Api.ControllerHelpers; using AyaNova.Models; using System.Linq; using System; using Newtonsoft.Json.Linq; using System.Collections.Generic; namespace AyaNova.Biz { /* ############### MUST be able to update any section at any level independantly Fetch must be able to get entire graph or from any level down or single header only as required probably need a woheaderfetch route separately */ internal class WorkOrderBiz : BizObject, IJobObject, ISearchAbleObject, IReportAbleObject, IExportAbleObject, INotifiableObject { // //Feature specific roles // internal static AuthorizationRoles RolesAllowedToChangeSerial = AuthorizationRoles.BizAdminFull | AuthorizationRoles.DispatchFull | AuthorizationRoles.AccountingFull; internal WorkOrderBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles) { ct = dbcontext; UserId = currentUserId; UserTranslationId = userTranslationId; CurrentUserRoles = UserRoles; BizType = AyaType.WorkOrder; } internal static WorkOrderBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null) { if (httpContext != null) return new WorkOrderBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items)); else//when called internally for internal ops there will be no context so need to set default values for that return new WorkOrderBiz(ct, 1, ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdminFull); } /* ██╗ ██╗ ██████╗ ██████╗ ██╗ ██╗ ██████╗ ██████╗ ██████╗ ███████╗██████╗ ██║ ██║██╔═══██╗██╔══██╗██║ ██╔╝ ██╔═══██╗██╔══██╗██╔══██╗██╔════╝██╔══██╗ ██║ █╗ ██║██║ ██║██████╔╝█████╔╝█████╗██║ ██║██████╔╝██║ ██║█████╗ ██████╔╝ ██║███╗██║██║ ██║██╔══██╗██╔═██╗╚════╝██║ ██║██╔══██╗██║ ██║██╔══╝ ██╔══██╗ ╚███╔███╔╝╚██████╔╝██║ ██║██║ ██╗ ╚██████╔╝██║ ██║██████╔╝███████╗██║ ██║ ╚══╝╚══╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝ ╚══════╝╚═╝ ╚═╝ */ #region WorkOrder level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task WorkOrderExistsAsync(long id) { return await ct.WorkOrder.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task WorkOrderCreateAsync(WorkOrder newObject) { await WorkOrderValidateAsync(newObject, null); if (HasErrors) return null; else { newObject.Tags = TagBiz.NormalizeTags(newObject.Tags); newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields); await ct.WorkOrder.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, AyaEvent.Created), ct); await WorkOrderSearchIndexAsync(newObject, true); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null); await HandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// //DUPLICATE // internal async Task WorkOrderDuplicateAsync(long id) { WorkOrder dbObject = await WorkOrderGetAsync(id, false); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } WorkOrder newObject = new WorkOrder(); CopyObject.Copy(dbObject, newObject, "Wiki,Serial"); newObject.Id = 0; newObject.Concurrency = 0; await ct.WorkOrder.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, AyaEvent.Created), ct); await WorkOrderSearchIndexAsync(newObject, true); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null); await HandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task WorkOrderGetAsync(long id, bool logTheGetEvent = true) { //Note: there could be rules checking here in future, i.e. can only get own workorder or something //if so, then need to implement AddError and in route handle Null return with Error check just like PUT route does now //https://docs.microsoft.com/en-us/ef/core/querying/related-data //docs say this will not query twice but will recognize the duplicate woitem bit which is required for multiple grandchild collections var ret = await ct.WorkOrder.AsNoTracking() .Include(w => w.Items) .ThenInclude(wi => wi.Expenses) .Include(w => w.Items) .ThenInclude(wi => wi.Labors) .Include(w => w.Items) .ThenInclude(wi => wi.Loans) .Include(w => w.Items) .ThenInclude(wi => wi.Parts) .Include(w => w.Items) .ThenInclude(wi => wi.PartRequests) .Include(w => w.Items) .ThenInclude(wi => wi.ScheduledUsers) .Include(w => w.Items) .ThenInclude(wi => wi.Tasks) .Include(w => w.Items) .ThenInclude(wi => wi.Travels) .Include(w => w.Items) .ThenInclude(wi => wi.Units) .SingleOrDefaultAsync(z => z.Id == id); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task WorkOrderPutAsync(WorkOrder putObject) { WorkOrder dbObject = await WorkOrderGetAsync(putObject.Id, false); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } if (dbObject.Concurrency != putObject.Concurrency) { AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } putObject.Tags = TagBiz.NormalizeTags(putObject.Tags); putObject.CustomFields = JsonUtil.CompactJson(putObject.CustomFields); await WorkOrderValidateAsync(putObject, dbObject); if (HasErrors) return null; ct.Replace(dbObject, putObject); try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await WorkOrderExistsAsync(putObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, BizType, AyaEvent.Modified), ct); await WorkOrderSearchIndexAsync(putObject, false); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags); await HandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject); return putObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task WorkOrderDeleteAsync(long id) { using (var transaction = await ct.Database.BeginTransactionAsync()) { try { WorkOrder dbObject = await ct.WorkOrder.SingleOrDefaultAsync(z => z.Id == id); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND); return false; } WorkOrderValidateCanDelete(dbObject); if (HasErrors) return false; //collect the child id's to delete var ItemIds = await ct.WorkOrderItem.AsNoTracking().Where(z => z.WorkOrderId == id).Select(z => z.Id).ToListAsync(); //Delete children foreach (long ItemId in ItemIds) if (!await ItemDeleteAsync(ItemId, transaction)) return false; ct.WorkOrder.Remove(dbObject); await ct.SaveChangesAsync(); await EventLogProcessor.DeleteObjectLogAsync(UserId, BizType, dbObject.Id, dbObject.Serial.ToString(), ct); await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, BizType, ct); await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags); await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct); await transaction.CommitAsync(); await HandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); } catch { //Just re-throw for now, let exception handler deal, but in future may want to deal with this more here throw; } return true; } } //////////////////////////////////////////////////////////////////////////////////////////////// //GET ANCESTOR TYPE AND ID // internal static async Task GetAncestor(AyaType ayaType, long id, AyContext ct) { long woitemid = 0; switch (ayaType) { case AyaType.WorkOrderItem: woitemid = id; break; case AyaType.WorkOrderItemExpense: woitemid = await ct.WorkOrderItemExpense.Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync(); break; case AyaType.WorkOrderItemLabor: woitemid = await ct.WorkOrderItemLabor.Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync(); break; case AyaType.WorkOrderItemLoan: woitemid = await ct.WorkOrderItemLoan.Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync(); break; case AyaType.WorkOrderItemPart: woitemid = await ct.WorkOrderItemPart.Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync(); break; case AyaType.WorkOrderItemPartRequest: woitemid = await ct.WorkOrderItemPartRequest.Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync(); break; case AyaType.WorkOrderItemScheduledUser: woitemid = await ct.WorkOrderItemScheduledUser.Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync(); break; case AyaType.WorkOrderItemTask: woitemid = await ct.WorkOrderItemTask.Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync(); break; case AyaType.WorkOrderItemTravel: woitemid = await ct.WorkOrderItemTravel.Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync(); break; default: throw new System.NotSupportedException($"WorkOrderBiz::GetAncestor -> AyaType {ayaType.ToString()} is not supported"); } return new AyaTypeId(AyaType.WorkOrder, await ct.WorkOrderItem.Where(z => z.Id == woitemid).Select(z => z.WorkOrderId).SingleOrDefaultAsync()); } //////////////////////////////////////////////////////////////////////////////////////////////// //SEARCH // private async Task WorkOrderSearchIndexAsync(WorkOrder obj, bool isNew) { var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, BizType); SearchParams.AddText(obj.Notes).AddText(obj.Serial).AddText(obj.Wiki).AddText(obj.Tags).AddCustomFields(obj.CustomFields); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task GetSearchResultSummary(long id) { var obj = await ct.WorkOrder.SingleOrDefaultAsync(z => z.Id == id); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.Notes).AddText(obj.Serial).AddText(obj.Wiki).AddText(obj.Tags).AddCustomFields(obj.CustomFields); return SearchParams; } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // //Can save or update? private async Task WorkOrderValidateAsync(WorkOrder proposedObj, WorkOrder currentObj) { //run validation and biz rules bool isNew = currentObj == null; /* todo: workorder status list first, it's a table of created items, keep properties from v7 but add the following properties: SelectRoles - who can select the status (still shows if they can't select but that's the current status, like active does) This is best handled at the client. It prefetches all the status out of the normal picklist process, more like how other things are separately handled now without a picklist client then knows if a status is available or not and can process to only present available ones #### Server can use a biz rule to ensure that it can't be circumvented UI defaults to any role DeselectRoles - who can unset this status (important for process control) UI defaults to any role CompletedStatus bool - this is a final status indicating all work on the workorder is completed, affects notification etc UI defaults to false but when set to true auto sets lockworkorder to true (but user can just unset lockworkorder) LockWorkorder - this status is considered read only and the workorder is locked Just a read only thing, can just change status to "unlock" it to support states where no one should work on a wo for whatever reason but it's not necessarily completed e.g. "Hold for inspection", "On hold" generally etc */ // //Name required // if (string.IsNullOrWhiteSpace(proposedObj.Name)) // AddError(ApiErrorCode.VALIDATION_REQUIRED, "Name"); // //If name is otherwise OK, check that name is unique // if (!PropertyHasErrors("Name")) // { // //Use Any command is efficient way to check existance, it doesn't return the record, just a true or false // if (await ct.WorkOrder.AnyAsync(z => z.Name == proposedObj.Name && z.Id != proposedObj.Id)) // { // AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, "Name"); // } // } //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrder.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj); //validate custom fields CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields); } } private void WorkOrderValidateCanDelete(WorkOrder dbObject) { //whatever needs to be check to delete this object } //############### NOTIFICATION TODO /* todo: workorder notifications remove #30 and #32 as redundant WorkorderStatusChange = 4,//* Workorder object, any *change* of status including from no status (new) to a specific conditional status ID value WorkorderStatusAge = 24,//* Workorder object Created / Updated, conditional on exact status selected IdValue, Tags conditional, advance notice can be set //THESE TWO ARE REDUNDANT: this is actually workorderstatuschange because can just pick any status under workorderstatuschange to be notified about WorkorderFinished = 30, //*Service work order is set to any status that is flagged as a "Finished" type of status. Customer & User //This one could be accomplished with WorkorderStatusAge, just pick a finished status and set a time frame and wala! WorkorderFinishedFollowUp = 32, //* Service workorder closed status follow up again after this many TIMESPAN todo: CHANGE WorkorderFinishStatusOverdue = 15,//* Workorder object not set to a "Finished" flagged workorder status type in selected time span from creation of workorder Change this to a new type that is based on so many days *without* being set to a particular status but first check if tied to contract response time stuff, how that's handled that's closeby date in v7 but isn't that deprecated now without a "close"? maybe I do need the Finished status bool thing above */ //////////////////////////////////////////////////////////////////////////////////////////////// //REPORTING // public async Task GetReportData(long[] idList) { JArray ReportData = new JArray(); while (idList.Any()) { var batch = idList.Take(IReportAbleObject.REPORT_DATA_BATCH_SIZE); idList = idList.Skip(IReportAbleObject.REPORT_DATA_BATCH_SIZE).ToArray(); //query for this batch, comes back in db natural order unfortunately var batchResults = await ct.WorkOrder.AsNoTracking().Where(z => batch.Contains(z.Id)).ToArrayAsync(); //order the results back into original var orderedList = from id in batch join z in batchResults on id equals z.Id select z; foreach (WorkOrder w in orderedList) { await PopulateVizFields(w); var jo = JObject.FromObject(w); if (!JsonUtil.JTokenIsNullOrEmpty(jo["CustomFields"])) jo["CustomFields"] = JObject.Parse((string)jo["CustomFields"]); ReportData.Add(jo); } } return ReportData; } //populate viz fields from provided object private async Task PopulateVizFields(WorkOrder o) { // if (o.WorkOrderOverseerId != null) // o.WorkOrderOverseerViz = await ct.User.AsNoTracking().Where(x => x.Id == o.WorkOrderOverseerId).Select(x => x.Name).FirstOrDefaultAsync(); } //////////////////////////////////////////////////////////////////////////////////////////////// // IMPORT EXPORT // public async Task GetExportData(long[] idList) { //for now just re-use the report data code //this may turn out to be the pattern for most biz object types but keeping it seperate allows for custom usage from time to time return await GetReportData(idList); } // public async Task> ImportData(JArray ja) // { // List ImportResult = new List(); // string ImportTag = $"imported-{FileUtil.GetSafeDateFileName()}"; // var jsset = JsonSerializer.CreateDefault(new JsonSerializerSettings { ContractResolver = new AyaNova.Util.JsonUtil.ShouldSerializeContractResolver(new string[] { "Concurrency", "Id", "CustomFields" }) }); // foreach (JObject j in ja) // { // var w = j.ToObject(jsset); // if (j["CustomFields"] != null) // w.CustomFields = j["CustomFields"].ToString(); // w.Tags.Add(ImportTag);//so user can find them all and revert later if necessary // var res = await WorkOrderCreateAsync(w); // if (res == null) // { // ImportResult.Add($"* {w.Serial} - {this.GetErrorsAsString()}"); // this.ClearErrors(); // } // else // { // ImportResult.Add($"{w.Serial} - ok"); // } // } // return ImportResult; // } //////////////////////////////////////////////////////////////////////////////////////////////// //JOB / OPERATIONS // public async Task HandleJobAsync(OpsJob job) { switch (job.JobType) { case JobType.BatchCoreObjectOperation: await ProcessBatchJobAsync(job); break; default: throw new System.ArgumentOutOfRangeException($"WorkOrder.HandleJob-> Invalid job type{job.JobType.ToString()}"); } } private async Task ProcessBatchJobAsync(OpsJob job) { await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Running); await JobsBiz.LogJobAsync(job.GId, $"LT:StartJob {job.SubType}"); List idList = new List(); long FailedObjectCount = 0; JObject jobData = JObject.Parse(job.JobInfo); if (jobData.ContainsKey("idList")) idList = ((JArray)jobData["idList"]).ToObject>(); else idList = await ct.Widget.Select(z => z.Id).ToListAsync(); bool SaveIt = false; foreach (long id in idList) { try { SaveIt = false; ClearErrors(); ICoreBizObjectModel o = null; //save a fetch if it's a delete if (job.SubType != JobSubType.Delete) o = await GetWorkOrderGraphItem(job.AType, id); switch (job.SubType) { case JobSubType.TagAddAny: case JobSubType.TagAdd: case JobSubType.TagRemoveAny: case JobSubType.TagRemove: case JobSubType.TagReplaceAny: case JobSubType.TagReplace: SaveIt = TagBiz.ProcessBatchTagOperation(o.Tags, (string)jobData["tag"], jobData.ContainsKey("toTag") ? (string)jobData["toTag"] : null, job.SubType); break; case JobSubType.Delete: if (!await DeleteWorkOrderGraphItem(job.AType, id)) { await JobsBiz.LogJobAsync(job.GId, $"LT:Errors {GetErrorsAsString()} id {id}"); FailedObjectCount++; } break; default: throw new System.ArgumentOutOfRangeException($"ProcessBatchJobAsync -> Invalid job Subtype{job.SubType}"); } if (SaveIt) { o = await PutWorkOrderGraphItem(job.AType, o); if (o == null) { await JobsBiz.LogJobAsync(job.GId, $"LT:Errors {GetErrorsAsString()} id {id}"); FailedObjectCount++; } } } catch (Exception ex) { await JobsBiz.LogJobAsync(job.GId, $"LT:Errors id({id})"); await JobsBiz.LogJobAsync(job.GId, ExceptionUtil.ExtractAllExceptionMessages(ex)); } } await JobsBiz.LogJobAsync(job.GId, $"LT:BatchJob {job.SubType} {idList.Count}{(FailedObjectCount > 0 ? " - LT:Failed " + FailedObjectCount : "")}"); await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Completed); } //////////////////////////////////////////////////////////////////////////////////////////////// // NOTIFICATION PROCESSING // public async Task HandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null) { ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger(); if (ServerBootConfig.SEEDING) return; log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{this.BizType}, AyaEvent:{ayaEvent}]"); bool isNew = currentObj == null; //STANDARD EVENTS FOR ALL OBJECTS await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct); //SPECIFIC EVENTS FOR THIS OBJECT //todo: contract response time notification }//end of process notifications #endregion workorder level /* ██╗████████╗███████╗███╗ ███╗███████╗ ██║╚══██╔══╝██╔════╝████╗ ████║██╔════╝ ██║ ██║ █████╗ ██╔████╔██║███████╗ ██║ ██║ ██╔══╝ ██║╚██╔╝██║╚════██║ ██║ ██║ ███████╗██║ ╚═╝ ██║███████║ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚══════╝ */ #region WorkOrderItem level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task ItemExistsAsync(long id) { return await ct.WorkOrderItem.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task ItemCreateAsync(WorkOrderItem dtNewObject) { WorkOrderItem newObject = new WorkOrderItem(); CopyObject.Copy(dtNewObject, newObject); await ItemValidateAsync(newObject, null); if (HasErrors) return null; else { newObject.Tags = TagBiz.NormalizeTags(newObject.Tags); newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields); await ct.WorkOrderItem.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, AyaType.WorkOrderItem, AyaEvent.Created), ct); await ItemSearchIndexAsync(newObject, true); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null); await HandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task ItemGetAsync(long id, bool logTheGetEvent = true) { //Note: there could be rules checking here in future, i.e. can only get own workorder or something //if so, then need to implement AddError and in route handle Null return with Error check just like PUT route does now //https://docs.microsoft.com/en-us/ef/core/querying/related-data //docs say this will not query twice but will recognize the duplicate woitem bit which is required for multiple grandchild collections var ret = await ct.WorkOrderItem .Include(wi => wi.Expenses) .Include(wi => wi.Labors) .Include(wi => wi.Loans) .Include(wi => wi.Parts) .Include(wi => wi.PartRequests) .Include(wi => wi.ScheduledUsers) .Include(wi => wi.Tasks) .Include(wi => wi.Travels) .Include(wi => wi.Units) .SingleOrDefaultAsync(z => z.Id == id); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, AyaType.WorkOrderItem, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task ItemPutAsync(WorkOrderItem dtPutObject) { WorkOrderItem dbObject = await ct.WorkOrderItem.SingleOrDefaultAsync(z => z.Id == dtPutObject.Id); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } // make a snapshot of the original for validation but update the original to preserve workflow WorkOrderItem SnapshotOfOriginalDBObj = new WorkOrderItem(); CopyObject.Copy(dbObject, SnapshotOfOriginalDBObj); //Replace the db object with the PUT object CopyObject.Copy(dtPutObject, dbObject, "Id"); dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags); dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields); ct.Entry(dbObject).OriginalValues["Concurrency"] = dtPutObject.Concurrency; await ItemValidateAsync(dbObject, SnapshotOfOriginalDBObj); if (HasErrors) return null; try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await ItemExistsAsync(dtPutObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, AyaType.WorkOrderItem, AyaEvent.Modified), ct); await ItemSearchIndexAsync(dbObject, false); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags); await HandlePotentialNotificationEvent(AyaEvent.Modified, dbObject, SnapshotOfOriginalDBObj); return dbObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task ItemDeleteAsync(long id, Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction parentTransaction = null) { Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction transaction = null; if (parentTransaction == null) transaction = await ct.Database.BeginTransactionAsync(); try { WorkOrderItem dbObject = await ct.WorkOrderItem.SingleOrDefaultAsync(z => z.Id == id); ItemValidateCanDelete(dbObject); if (HasErrors) return false; //collect the child id's to delete var ExpenseIds = await ct.WorkOrderItemExpense.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync(); var LaborIds = await ct.WorkOrderItemLabor.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync(); var LoanIds = await ct.WorkOrderItemLoan.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync(); var PartIds = await ct.WorkOrderItemPart.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync(); var PartRequestIds = await ct.WorkOrderItemPartRequest.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync(); var ScheduledUserIds = await ct.WorkOrderItemScheduledUser.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync(); var TaskIds = await ct.WorkOrderItemTask.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync(); var TravelIds = await ct.WorkOrderItemTravel.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync(); var UnitIds = await ct.WorkOrderItemUnit.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync(); //Delete children foreach (long ItemId in ExpenseIds) if (!await ExpenseDeleteAsync(ItemId)) return false; foreach (long ItemId in LaborIds) if (!await LaborDeleteAsync(ItemId)) return false; foreach (long ItemId in LoanIds) if (!await LoanDeleteAsync(ItemId)) return false; foreach (long ItemId in PartIds) if (!await PartDeleteAsync(ItemId)) return false; foreach (long ItemId in PartRequestIds) if (!await PartRequestDeleteAsync(ItemId)) return false; foreach (long ItemId in ScheduledUserIds) if (!await ScheduledUserDeleteAsync(ItemId)) return false; foreach (long ItemId in TaskIds) if (!await TaskDeleteAsync(ItemId)) return false; foreach (long ItemId in TravelIds) if (!await TravelDeleteAsync(ItemId)) return false; foreach (long ItemId in UnitIds) if (!await UnitDeleteAsync(ItemId)) return false; ct.WorkOrderItem.Remove(dbObject); await ct.SaveChangesAsync(); //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, AyaType.WorkOrderItem, dbObject.Id, "wo:" + dbObject.WorkOrderId.ToString(), ct); await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, AyaType.WorkOrderItem, ct); await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags); await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct); //all good do the commit if it's ours if (parentTransaction == null) await transaction.CommitAsync(); await HandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); } catch { //Just re-throw for now, let exception handler deal, but in future may want to deal with this more here throw; } finally { if (parentTransaction == null) await transaction.DisposeAsync(); } return true; } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task ItemValidateAsync(WorkOrderItem proposedObj, WorkOrderItem currentObj) { //run validation and biz rules bool isNew = currentObj == null; //does it have a valid workorder id if (proposedObj.WorkOrderId == 0) AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderId"); else if (!await WorkOrderExistsAsync(proposedObj.WorkOrderId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderId"); } //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItem.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);//note: this is passed only to add errors //validate custom fields CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields); } } private void ItemValidateCanDelete(WorkOrderItem obj) { if (obj == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return; } //re-check rights here necessary due to traversal delete from Principle object if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItem)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } private async Task ItemSearchIndexAsync(WorkOrderItem obj, bool isNew) { //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, AyaType.WorkOrderItem); SearchParams.AddText(obj.Notes).AddText(obj.Wiki).AddText(obj.Tags).AddCustomFields(obj.CustomFields); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task ItemGetSearchResultSummary(long id) { var obj = await ct.WorkOrderItem.SingleOrDefaultAsync(z => z.Id == id); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.Notes).AddText(obj.Wiki).AddText(obj.Tags).AddCustomFields(obj.CustomFields); return SearchParams; } #endregion work order item level /* ███████╗██╗ ██╗██████╗ ███████╗███╗ ██╗███████╗███████╗███████╗ ██╔════╝╚██╗██╔╝██╔══██╗██╔════╝████╗ ██║██╔════╝██╔════╝██╔════╝ █████╗ ╚███╔╝ ██████╔╝█████╗ ██╔██╗ ██║███████╗█████╗ ███████╗ ██╔══╝ ██╔██╗ ██╔═══╝ ██╔══╝ ██║╚██╗██║╚════██║██╔══╝ ╚════██║ ███████╗██╔╝ ██╗██║ ███████╗██║ ╚████║███████║███████╗███████║ ╚══════╝╚═╝ ╚═╝╚═╝ ╚══════╝╚═╝ ╚═══╝╚══════╝╚══════╝╚══════╝ */ #region WorkOrderItemExpense level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task ExpenseExistsAsync(long id) { return await ct.WorkOrderItemExpense.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task ExpenseCreateAsync(WorkOrderItemExpense newObject) { await ExpenseValidateAsync(newObject, null); if (HasErrors) return null; else { newObject.Tags = TagBiz.NormalizeTags(newObject.Tags); newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields); await ct.WorkOrderItemExpense.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, AyaType.WorkOrderItemExpense, AyaEvent.Created), ct); await ExpenseSearchIndexAsync(newObject, true); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null); await HandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task ExpenseGetAsync(long id, bool logTheGetEvent = true) { //Note: there could be rules checking here in future, i.e. can only get own workorder or something //if so, then need to implement AddError and in route handle Null return with Error check just like PUT route does now //https://docs.microsoft.com/en-us/ef/core/querying/related-data //docs say this will not query twice but will recognize the duplicate woitem bit which is required for multiple grandchild collections var ret = await ct.WorkOrderItemExpense .SingleOrDefaultAsync(z => z.Id == id); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, AyaType.WorkOrderItemExpense, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task ExpensePutAsync(WorkOrderItemExpense dtPutObject) { WorkOrderItemExpense dbObject = await ct.WorkOrderItemExpense.SingleOrDefaultAsync(z => z.Id == dtPutObject.Id); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } // make a snapshot of the original for validation but update the original to preserve workflow WorkOrderItemExpense SnapshotOfOriginalDBObj = new WorkOrderItemExpense(); CopyObject.Copy(dbObject, SnapshotOfOriginalDBObj); CopyObject.Copy(dtPutObject, dbObject, "Id"); dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags); dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields); ct.Entry(dbObject).OriginalValues["Concurrency"] = dtPutObject.Concurrency; await ExpenseValidateAsync(dbObject, SnapshotOfOriginalDBObj); if (HasErrors) return null; try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await ExpenseExistsAsync(dtPutObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, AyaType.WorkOrderItemExpense, AyaEvent.Modified), ct); await ExpenseSearchIndexAsync(dbObject, false); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags); await HandlePotentialNotificationEvent(AyaEvent.Modified, dbObject, SnapshotOfOriginalDBObj); return dbObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task ExpenseDeleteAsync(long id) { WorkOrderItemExpense dbObject = await ct.WorkOrderItemExpense.SingleOrDefaultAsync(z => z.Id == id); ExpenseValidateCanDelete(dbObject); if (HasErrors) return false; ct.WorkOrderItemExpense.Remove(dbObject); await ct.SaveChangesAsync(); //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, AyaType.WorkOrderItemExpense, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct); await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, AyaType.WorkOrderItemExpense, ct); await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags); await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct); await HandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); return true; } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task ExpenseValidateAsync(WorkOrderItemExpense proposedObj, WorkOrderItemExpense currentObj) { //run validation and biz rules bool isNew = currentObj == null; if (proposedObj.WorkOrderItemId == 0) AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId"); else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId"); } //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemExpense.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);//note: this is passed only to add errors //validate custom fields CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields); } } private void ExpenseValidateCanDelete(WorkOrderItemExpense obj) { if (obj == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return; } //re-check rights here necessary due to traversal delete from Principle object if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItemExpense)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } ////////////////////////////////////////////// //INDEXING // private async Task ExpenseSearchIndexAsync(WorkOrderItemExpense obj, bool isNew) { //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, AyaType.WorkOrderItemExpense); SearchParams.AddText(obj.Notes).AddText(obj.Tags).AddCustomFields(obj.CustomFields); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task ExpenseGetSearchResultSummary(long id) { var obj = await ct.WorkOrderItemExpense.SingleOrDefaultAsync(z => z.Id == id); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.Notes).AddText(obj.Tags).AddCustomFields(obj.CustomFields); return SearchParams; } #endregion work order item LABOR level /* ██╗ █████╗ ██████╗ ██████╗ ██████╗ ██║ ██╔══██╗██╔══██╗██╔═══██╗██╔══██╗ ██║ ███████║██████╔╝██║ ██║██████╔╝ ██║ ██╔══██║██╔══██╗██║ ██║██╔══██╗ ███████╗██║ ██║██████╔╝╚██████╔╝██║ ██║ ╚══════╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═╝ ╚═╝ */ #region WorkOrderItemLabor level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task LaborExistsAsync(long id) { return await ct.WorkOrderItemLabor.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task LaborCreateAsync(WorkOrderItemLabor newObject) { await LaborValidateAsync(newObject, null); if (HasErrors) return null; else { newObject.Tags = TagBiz.NormalizeTags(newObject.Tags); newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields); await ct.WorkOrderItemLabor.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, AyaType.WorkOrderItemLabor, AyaEvent.Created), ct); await LaborSearchIndexAsync(newObject, true); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null); await HandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task LaborGetAsync(long id, bool logTheGetEvent = true) { //Note: there could be rules checking here in future, i.e. can only get own workorder or something //if so, then need to implement AddError and in route handle Null return with Error check just like PUT route does now //https://docs.microsoft.com/en-us/ef/core/querying/related-data //docs say this will not query twice but will recognize the duplicate woitem bit which is required for multiple grandchild collections var ret = await ct.WorkOrderItemLabor .SingleOrDefaultAsync(z => z.Id == id); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, AyaType.WorkOrderItemLabor, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task LaborPutAsync(WorkOrderItemLabor dtPutObject) { WorkOrderItemLabor dbObject = await ct.WorkOrderItemLabor.SingleOrDefaultAsync(z => z.Id == dtPutObject.Id); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } // make a snapshot of the original for validation but update the original to preserve workflow WorkOrderItemLabor SnapshotOfOriginalDBObj = new WorkOrderItemLabor(); CopyObject.Copy(dbObject, SnapshotOfOriginalDBObj); CopyObject.Copy(dtPutObject, dbObject, "Id"); dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags); dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields); ct.Entry(dbObject).OriginalValues["Concurrency"] = dtPutObject.Concurrency; await LaborValidateAsync(dbObject, SnapshotOfOriginalDBObj); if (HasErrors) return null; try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await LaborExistsAsync(dtPutObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, AyaType.WorkOrderItemLabor, AyaEvent.Modified), ct); await LaborSearchIndexAsync(dbObject, false); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags); await HandlePotentialNotificationEvent(AyaEvent.Modified, dbObject, SnapshotOfOriginalDBObj); return dbObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task LaborDeleteAsync(long id) { WorkOrderItemLabor dbObject = await ct.WorkOrderItemLabor.SingleOrDefaultAsync(z => z.Id == id); LaborValidateCanDelete(dbObject); if (HasErrors) return false; ct.WorkOrderItemLabor.Remove(dbObject); await ct.SaveChangesAsync(); //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, AyaType.WorkOrderItemLabor, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct); await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, AyaType.WorkOrderItemLabor, ct); await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags); await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct); await HandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); return true; } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task LaborValidateAsync(WorkOrderItemLabor proposedObj, WorkOrderItemLabor currentObj) { //run validation and biz rules bool isNew = currentObj == null; if (proposedObj.WorkOrderItemId == 0) AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId"); else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId"); } //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemLabor.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);//note: this is passed only to add errors //validate custom fields CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields); } } private void LaborValidateCanDelete(WorkOrderItemLabor obj) { if (obj == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return; } //re-check rights here necessary due to traversal delete from Principle object if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItemLabor)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } ////////////////////////////////////////////// //INDEXING // private async Task LaborSearchIndexAsync(WorkOrderItemLabor obj, bool isNew) { //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, AyaType.WorkOrderItemLabor); SearchParams.AddText(obj.Notes).AddText(obj.Tags).AddCustomFields(obj.CustomFields); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task LaborGetSearchResultSummary(long id) { var obj = await ct.WorkOrderItemLabor.SingleOrDefaultAsync(z => z.Id == id); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.Notes).AddText(obj.Tags).AddCustomFields(obj.CustomFields); return SearchParams; } #endregion work order item LABOR level /* ██╗ ██████╗ █████╗ ███╗ ██╗ ██║ ██╔═══██╗██╔══██╗████╗ ██║ ██║ ██║ ██║███████║██╔██╗ ██║ ██║ ██║ ██║██╔══██║██║╚██╗██║ ███████╗╚██████╔╝██║ ██║██║ ╚████║ */ #region WorkOrderItemLoan level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task LoanExistsAsync(long id) { return await ct.WorkOrderItemLoan.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task LoanCreateAsync(WorkOrderItemLoan newObject) { await LoanValidateAsync(newObject, null); if (HasErrors) return null; else { newObject.Tags = TagBiz.NormalizeTags(newObject.Tags); newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields); await ct.WorkOrderItemLoan.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, AyaType.WorkOrderItemLoan, AyaEvent.Created), ct); await LoanSearchIndexAsync(newObject, true); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null); await HandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task LoanGetAsync(long id, bool logTheGetEvent = true) { //Note: there could be rules checking here in future, i.e. can only get own workorder or something //if so, then need to implement AddError and in route handle Null return with Error check just like PUT route does now //https://docs.microsoft.com/en-us/ef/core/querying/related-data //docs say this will not query twice but will recognize the duplicate woitem bit which is required for multiple grandchild collections var ret = await ct.WorkOrderItemLoan .SingleOrDefaultAsync(z => z.Id == id); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, AyaType.WorkOrderItemLoan, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task LoanPutAsync(WorkOrderItemLoan dtPutObject) { WorkOrderItemLoan dbObject = await ct.WorkOrderItemLoan.SingleOrDefaultAsync(z => z.Id == dtPutObject.Id); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } // make a snapshot of the original for validation but update the original to preserve workflow WorkOrderItemLoan SnapshotOfOriginalDBObj = new WorkOrderItemLoan(); CopyObject.Copy(dbObject, SnapshotOfOriginalDBObj); CopyObject.Copy(dtPutObject, dbObject, "Id"); dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags); dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields); ct.Entry(dbObject).OriginalValues["Concurrency"] = dtPutObject.Concurrency; await LoanValidateAsync(dbObject, SnapshotOfOriginalDBObj); if (HasErrors) return null; try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await LoanExistsAsync(dtPutObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, AyaType.WorkOrderItemLoan, AyaEvent.Modified), ct); await LoanSearchIndexAsync(dbObject, false); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags); await HandlePotentialNotificationEvent(AyaEvent.Modified, dbObject, SnapshotOfOriginalDBObj); return dbObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task LoanDeleteAsync(long id) { WorkOrderItemLoan dbObject = await ct.WorkOrderItemLoan.SingleOrDefaultAsync(z => z.Id == id); LoanValidateCanDelete(dbObject); if (HasErrors) return false; ct.WorkOrderItemLoan.Remove(dbObject); await ct.SaveChangesAsync(); //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, AyaType.WorkOrderItemLoan, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct); await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, AyaType.WorkOrderItemLoan, ct); await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags); await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct); await HandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); return true; } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task LoanValidateAsync(WorkOrderItemLoan proposedObj, WorkOrderItemLoan currentObj) { //run validation and biz rules bool isNew = currentObj == null; if (proposedObj.WorkOrderItemId == 0) AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId"); else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId"); } //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemLoan.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);//note: this is passed only to add errors //validate custom fields CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields); } } private void LoanValidateCanDelete(WorkOrderItemLoan obj) { if (obj == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return; } //re-check rights here necessary due to traversal delete from Principle object if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItemLoan)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } ////////////////////////////////////////////// //INDEXING // private async Task LoanSearchIndexAsync(WorkOrderItemLoan obj, bool isNew) { //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, AyaType.WorkOrderItemLoan); SearchParams.AddText(obj.Notes).AddText(obj.Tags).AddCustomFields(obj.CustomFields); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task LoanGetSearchResultSummary(long id) { var obj = await ct.WorkOrderItemLoan.SingleOrDefaultAsync(z => z.Id == id); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.Notes).AddText(obj.Tags).AddCustomFields(obj.CustomFields); return SearchParams; } #endregion work order item LABOR level /* ██████╗ █████╗ ██████╗ ████████╗███████╗ ██╔══██╗██╔══██╗██╔══██╗╚══██╔══╝██╔════╝ ██████╔╝███████║██████╔╝ ██║ ███████╗ ██╔═══╝ ██╔══██║██╔══██╗ ██║ ╚════██║ ██║ ██║ ██║██║ ██║ ██║ ███████║ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚══════╝ */ #region WorkOrderItemPart level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task PartExistsAsync(long id) { return await ct.WorkOrderItemPart.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task CreatePartAsync(WorkOrderItemPart newObject) { await PartValidateAsync(newObject, null); if (HasErrors) return null; else { newObject.Tags = TagBiz.NormalizeTags(newObject.Tags); newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields); await ct.WorkOrderItemPart.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, AyaType.WorkOrderItemPart, AyaEvent.Created), ct); await PartSearchIndexAsync(newObject, true); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task PartGetAsync(long id, bool logTheGetEvent = true) { //Note: there could be rules checking here in future, i.e. can only get own workorder or something //if so, then need to implement AddError and in route handle Null return with Error check just like PUT route does now //https://docs.microsoft.com/en-us/ef/core/querying/related-data //docs say this will not query twice but will recognize the duplicate woitem bit which is required for multiple grandchild collections var ret = await ct.WorkOrderItemPart .SingleOrDefaultAsync(z => z.Id == id); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, AyaType.WorkOrderItemPart, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task PartPutAsync(WorkOrderItemPart dtPutObject) { WorkOrderItemPart dbObject = await ct.WorkOrderItemPart.SingleOrDefaultAsync(z => z.Id == dtPutObject.Id); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } // make a snapshot of the original for validation but update the original to preserve workflow WorkOrderItemPart SnapshotOfOriginalDBObj = new WorkOrderItemPart(); CopyObject.Copy(dbObject, SnapshotOfOriginalDBObj); CopyObject.Copy(dtPutObject, dbObject, "Id"); dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags); dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields); ct.Entry(dbObject).OriginalValues["Concurrency"] = dtPutObject.Concurrency; await PartValidateAsync(dbObject, SnapshotOfOriginalDBObj); if (HasErrors) return null; try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await PartExistsAsync(dtPutObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, AyaType.WorkOrderItemPart, AyaEvent.Modified), ct); await PartSearchIndexAsync(dbObject, false); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags); await HandlePotentialNotificationEvent(AyaEvent.Modified, dbObject, SnapshotOfOriginalDBObj); return dbObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task PartDeleteAsync(long id) { WorkOrderItemPart dbObject = await ct.WorkOrderItemPart.SingleOrDefaultAsync(z => z.Id == id); PartValidateCanDelete(dbObject); if (HasErrors) return false; ct.WorkOrderItemPart.Remove(dbObject); await ct.SaveChangesAsync(); //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, AyaType.WorkOrderItemPart, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct); await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, AyaType.WorkOrderItemPart, ct); await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags); await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct); await HandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); return true; } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task PartValidateAsync(WorkOrderItemPart proposedObj, WorkOrderItemPart currentObj) { //run validation and biz rules bool isNew = currentObj == null; if (proposedObj.WorkOrderItemId == 0) AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId"); else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId"); } //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemPart.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);//note: this is passed only to add errors //validate custom fields CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields); } } private void PartValidateCanDelete(WorkOrderItemPart obj) { if (obj == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return; } //re-check rights here necessary due to traversal delete from Principle object if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItemPart)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } private async Task PartSearchIndexAsync(WorkOrderItemPart obj, bool isNew) { //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, AyaType.WorkOrderItemPart); SearchParams.AddText(obj.Notes).AddText(obj.Tags).AddCustomFields(obj.CustomFields); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task PartGetSearchResultSummary(long id) { var obj = await ct.WorkOrderItemPart.SingleOrDefaultAsync(z => z.Id == id); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.Notes).AddText(obj.Tags).AddCustomFields(obj.CustomFields); return SearchParams; } #endregion work order item LABOR level /* ██████╗ █████╗ ██████╗ ████████╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗███████╗███████╗████████╗███████╗ ██╔══██╗██╔══██╗██╔══██╗╚══██╔══╝ ██╔══██╗██╔════╝██╔═══██╗██║ ██║██╔════╝██╔════╝╚══██╔══╝██╔════╝ ██████╔╝███████║██████╔╝ ██║█████╗██████╔╝█████╗ ██║ ██║██║ ██║█████╗ ███████╗ ██║ ███████╗ ██╔═══╝ ██╔══██║██╔══██╗ ██║╚════╝██╔══██╗██╔══╝ ██║▄▄ ██║██║ ██║██╔══╝ ╚════██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║ ██║ ██║ ██║███████╗╚██████╔╝╚██████╔╝███████╗███████║ ██║ ███████║ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝ ╚══▀▀═╝ ╚═════╝ ╚══════╝╚══════╝ ╚═╝ ╚══════╝ */ #region WorkOrderItemPartRequest level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task PartRequestExistsAsync(long id) { return await ct.WorkOrderItemPartRequest.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task PartRequestCreateAsync(WorkOrderItemPartRequest newObject) { await PartRequestValidateAsync(newObject, null); if (HasErrors) return null; else { newObject.Tags = TagBiz.NormalizeTags(newObject.Tags); newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields); await ct.WorkOrderItemPartRequest.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, AyaType.WorkOrderItemPartRequest, AyaEvent.Created), ct); await PartRequestSearchIndexAsync(newObject, true); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null); await HandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task PartRequestGetAsync(long id, bool logTheGetEvent = true) { //Note: there could be rules checking here in future, i.e. can only get own workorder or something //if so, then need to implement AddError and in route handle Null return with Error check just like PUT route does now //https://docs.microsoft.com/en-us/ef/core/querying/related-data //docs say this will not query twice but will recognize the duplicate woitem bit which is required for multiple grandchild collections var ret = await ct.WorkOrderItemPartRequest .SingleOrDefaultAsync(z => z.Id == id); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, AyaType.WorkOrderItemPartRequest, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task PartRequestPutAsync(WorkOrderItemPartRequest dtPutObject) { WorkOrderItemPartRequest dbObject = await ct.WorkOrderItemPartRequest.SingleOrDefaultAsync(z => z.Id == dtPutObject.Id); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } // make a snapshot of the original for validation but update the original to preserve workflow WorkOrderItemPartRequest SnapshotOfOriginalDBObj = new WorkOrderItemPartRequest(); CopyObject.Copy(dbObject, SnapshotOfOriginalDBObj); CopyObject.Copy(dtPutObject, dbObject, "Id"); dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags); dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields); ct.Entry(dbObject).OriginalValues["Concurrency"] = dtPutObject.Concurrency; await PartRequestValidateAsync(dbObject, SnapshotOfOriginalDBObj); if (HasErrors) return null; try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await PartRequestExistsAsync(dtPutObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, AyaType.WorkOrderItemPartRequest, AyaEvent.Modified), ct); await PartRequestSearchIndexAsync(dbObject, false); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags); await HandlePotentialNotificationEvent(AyaEvent.Modified, dbObject, SnapshotOfOriginalDBObj); return dbObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task PartRequestDeleteAsync(long id) { WorkOrderItemPartRequest dbObject = await ct.WorkOrderItemPartRequest.SingleOrDefaultAsync(z => z.Id == id); PartRequestValidateCanDelete(dbObject); if (HasErrors) return false; ct.WorkOrderItemPartRequest.Remove(dbObject); await ct.SaveChangesAsync(); //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, AyaType.WorkOrderItemPartRequest, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct); await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, AyaType.WorkOrderItemPartRequest, ct); await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags); await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct); await HandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); return true; } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task PartRequestValidateAsync(WorkOrderItemPartRequest proposedObj, WorkOrderItemPartRequest currentObj) { //run validation and biz rules bool isNew = currentObj == null; if (proposedObj.WorkOrderItemId == 0) AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId"); else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId"); } //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemPartRequest.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);//note: this is passed only to add errors //validate custom fields CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields); } } private void PartRequestValidateCanDelete(WorkOrderItemPartRequest obj) { if (obj == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return; } //re-check rights here necessary due to traversal delete from Principle object if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItemPartRequest)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } ////////////////////////////////////////////// //INDEXING // private async Task PartRequestSearchIndexAsync(WorkOrderItemPartRequest obj, bool isNew) { //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, AyaType.WorkOrderItemPartRequest); SearchParams.AddText(obj.Notes).AddText(obj.Tags).AddCustomFields(obj.CustomFields); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task PartRequestGetSearchResultSummary(long id) { var obj = await ct.WorkOrderItemPartRequest.SingleOrDefaultAsync(z => z.Id == id); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.Notes).AddText(obj.Tags).AddCustomFields(obj.CustomFields); return SearchParams; } #endregion work order item LABOR level /* ███████╗ ██████╗██╗ ██╗███████╗██████╗ ██╗ ██╗██╗ ███████╗██████╗ ██╗ ██╗███████╗███████╗██████╗ ███████╗ ██╔════╝██╔════╝██║ ██║██╔════╝██╔══██╗██║ ██║██║ ██╔════╝██╔══██╗ ██║ ██║██╔════╝██╔════╝██╔══██╗██╔════╝ ███████╗██║ ███████║█████╗ ██║ ██║██║ ██║██║ █████╗ ██║ ██║█████╗██║ ██║███████╗█████╗ ██████╔╝███████╗ ╚════██║██║ ██╔══██║██╔══╝ ██║ ██║██║ ██║██║ ██╔══╝ ██║ ██║╚════╝██║ ██║╚════██║██╔══╝ ██╔══██╗╚════██║ ███████║╚██████╗██║ ██║███████╗██████╔╝╚██████╔╝███████╗███████╗██████╔╝ ╚██████╔╝███████║███████╗██║ ██║███████║ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝╚══════╝ */ #region WorkOrderItemScheduledUser level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task ScheduledUserExistsAsync(long id) { return await ct.WorkOrderItemScheduledUser.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task ScheduledUserCreateAsync(WorkOrderItemScheduledUser newObject) { await ScheduledUserValidateAsync(newObject, null); if (HasErrors) return null; else { newObject.Tags = TagBiz.NormalizeTags(newObject.Tags); newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields); await ct.WorkOrderItemScheduledUser.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, AyaType.WorkOrderItemScheduledUser, AyaEvent.Created), ct); await ScheduledUserSearchIndexAsync(newObject, true); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null); await HandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task ScheduledUserGetAsync(long id, bool logTheGetEvent = true) { //Note: there could be rules checking here in future, i.e. can only get own workorder or something //if so, then need to implement AddError and in route handle Null return with Error check just like PUT route does now //https://docs.microsoft.com/en-us/ef/core/querying/related-data //docs say this will not query twice but will recognize the duplicate woitem bit which is required for multiple grandchild collections var ret = await ct.WorkOrderItemScheduledUser .SingleOrDefaultAsync(z => z.Id == id); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, AyaType.WorkOrderItemScheduledUser, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task ScheduledUserPutAsync(WorkOrderItemScheduledUser dtPutObject) { WorkOrderItemScheduledUser dbObject = await ct.WorkOrderItemScheduledUser.SingleOrDefaultAsync(z => z.Id == dtPutObject.Id); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } // make a snapshot of the original for validation but update the original to preserve workflow WorkOrderItemScheduledUser SnapshotOfOriginalDBObj = new WorkOrderItemScheduledUser(); CopyObject.Copy(dbObject, SnapshotOfOriginalDBObj); CopyObject.Copy(dtPutObject, dbObject, "Id"); dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags); dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields); ct.Entry(dbObject).OriginalValues["Concurrency"] = dtPutObject.Concurrency; await ScheduledUserValidateAsync(dbObject, SnapshotOfOriginalDBObj); if (HasErrors) return null; try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await ScheduledUserExistsAsync(dtPutObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, AyaType.WorkOrderItemScheduledUser, AyaEvent.Modified), ct); await ScheduledUserSearchIndexAsync(dbObject, false); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags); await HandlePotentialNotificationEvent(AyaEvent.Modified, dbObject, SnapshotOfOriginalDBObj); return dbObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task ScheduledUserDeleteAsync(long id) { WorkOrderItemScheduledUser dbObject = await ct.WorkOrderItemScheduledUser.SingleOrDefaultAsync(z => z.Id == id); ScheduledUserValidateCanDelete(dbObject); if (HasErrors) return false; ct.WorkOrderItemScheduledUser.Remove(dbObject); await ct.SaveChangesAsync(); //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, AyaType.WorkOrderItemScheduledUser, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct); await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, AyaType.WorkOrderItemScheduledUser, ct); await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags); await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct); await HandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); return true; } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task ScheduledUserValidateAsync(WorkOrderItemScheduledUser proposedObj, WorkOrderItemScheduledUser currentObj) { //run validation and biz rules bool isNew = currentObj == null; if (proposedObj.WorkOrderItemId == 0) AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId"); else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId"); } //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemScheduledUser.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);//note: this is passed only to add errors //validate custom fields CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields); } } private void ScheduledUserValidateCanDelete(WorkOrderItemScheduledUser obj) { if (obj == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return; } //re-check rights here necessary due to traversal delete from Principle object if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItemScheduledUser)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } ////////////////////////////////////////////// //INDEXING // private async Task ScheduledUserSearchIndexAsync(WorkOrderItemScheduledUser obj, bool isNew) { //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, AyaType.WorkOrderItemScheduledUser); SearchParams.AddText(obj.Notes).AddText(obj.Tags).AddCustomFields(obj.CustomFields); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task ScheduledUserGetSearchResultSummary(long id) { var obj = await ct.WorkOrderItemScheduledUser.SingleOrDefaultAsync(z => z.Id == id); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.Notes).AddText(obj.Tags).AddCustomFields(obj.CustomFields); return SearchParams; } #endregion work order item LABOR level /* ████████╗ █████╗ ███████╗██╗ ██╗ ╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ ██║ ███████║███████╗█████╔╝ ██║ ██╔══██║╚════██║██╔═██╗ ██║ ██║ ██║███████║██║ ██╗ ╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ */ #region WorkOrderItemTask level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task TaskExistsAsync(long id) { return await ct.WorkOrderItemTask.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task TaskCreateAsync(WorkOrderItemTask newObject) { await TaskValidateAsync(newObject, null); if (HasErrors) return null; else { newObject.Tags = TagBiz.NormalizeTags(newObject.Tags); newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields); await ct.WorkOrderItemTask.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, AyaType.WorkOrderItemTask, AyaEvent.Created), ct); await TaskSearchIndexAsync(newObject, true); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null); await HandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task TaskGetAsync(long id, bool logTheGetEvent = true) { //Note: there could be rules checking here in future, i.e. can only get own workorder or something //if so, then need to implement AddError and in route handle Null return with Error check just like PUT route does now //https://docs.microsoft.com/en-us/ef/core/querying/related-data //docs say this will not query twice but will recognize the duplicate woitem bit which is required for multiple grandchild collections var ret = await ct.WorkOrderItemTask .SingleOrDefaultAsync(z => z.Id == id); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, AyaType.WorkOrderItemTask, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task TaskPutAsync(WorkOrderItemTask dtPutObject) { WorkOrderItemTask dbObject = await ct.WorkOrderItemTask.SingleOrDefaultAsync(z => z.Id == dtPutObject.Id); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } // make a snapshot of the original for validation but update the original to preserve workflow WorkOrderItemTask SnapshotOfOriginalDBObj = new WorkOrderItemTask(); CopyObject.Copy(dbObject, SnapshotOfOriginalDBObj); CopyObject.Copy(dtPutObject, dbObject, "Id"); dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags); dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields); ct.Entry(dbObject).OriginalValues["Concurrency"] = dtPutObject.Concurrency; await TaskValidateAsync(dbObject, SnapshotOfOriginalDBObj); if (HasErrors) return null; try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await TaskExistsAsync(dtPutObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, AyaType.WorkOrderItemTask, AyaEvent.Modified), ct); await TaskSearchIndexAsync(dbObject, false); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags); await HandlePotentialNotificationEvent(AyaEvent.Modified, dbObject, SnapshotOfOriginalDBObj); return dbObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task TaskDeleteAsync(long id) { WorkOrderItemTask dbObject = await ct.WorkOrderItemTask.SingleOrDefaultAsync(z => z.Id == id); TaskValidateCanDelete(dbObject); if (HasErrors) return false; ct.WorkOrderItemTask.Remove(dbObject); await ct.SaveChangesAsync(); //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, AyaType.WorkOrderItemTask, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct); await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, AyaType.WorkOrderItemTask, ct); await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags); await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct); await HandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); return true; } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task TaskValidateAsync(WorkOrderItemTask proposedObj, WorkOrderItemTask currentObj) { //run validation and biz rules bool isNew = currentObj == null; if (proposedObj.WorkOrderItemId == 0) AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId"); else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId"); } //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemTask.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);//note: this is passed only to add errors //validate custom fields CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields); } } private void TaskValidateCanDelete(WorkOrderItemTask obj) { if (obj == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return; } //re-check rights here necessary due to traversal delete from Principle object if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItemTask)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } ////////////////////////////////////////////// //INDEXING // private async Task TaskSearchIndexAsync(WorkOrderItemTask obj, bool isNew) { //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, AyaType.WorkOrderItemTask); SearchParams.AddText(obj.Notes).AddText(obj.Tags).AddCustomFields(obj.CustomFields); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task TaskGetSearchResultSummary(long id) { var obj = await ct.WorkOrderItemTask.SingleOrDefaultAsync(z => z.Id == id); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.Notes).AddText(obj.Tags).AddCustomFields(obj.CustomFields); return SearchParams; } #endregion work order item LABOR level /* ████████╗██████╗ █████╗ ██╗ ██╗███████╗██╗ ╚══██╔══╝██╔══██╗██╔══██╗██║ ██║██╔════╝██║ ██║ ██████╔╝███████║██║ ██║█████╗ ██║ ██║ ██╔══██╗██╔══██║╚██╗ ██╔╝██╔══╝ ██║ ██║ ██║ ██║██║ ██║ ╚████╔╝ ███████╗███████╗ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚══════╝ */ #region WorkOrderItemTravel level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task TravelExistsAsync(long id) { return await ct.WorkOrderItemTravel.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task TravelCreateAsync(WorkOrderItemTravel newObject) { await TravelValidateAsync(newObject, null); if (HasErrors) return null; else { newObject.Tags = TagBiz.NormalizeTags(newObject.Tags); newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields); await ct.WorkOrderItemTravel.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, AyaType.WorkOrderItemTravel, AyaEvent.Created), ct); await TravelSearchIndexAsync(newObject, true); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null); await HandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task TravelGetAsync(long id, bool logTheGetEvent = true) { //Note: there could be rules checking here in future, i.e. can only get own workorder or something //if so, then need to implement AddError and in route handle Null return with Error check just like PUT route does now //https://docs.microsoft.com/en-us/ef/core/querying/related-data //docs say this will not query twice but will recognize the duplicate woitem bit which is required for multiple grandchild collections var ret = await ct.WorkOrderItemTravel .SingleOrDefaultAsync(z => z.Id == id); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, AyaType.WorkOrderItemTravel, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task TravelPutAsync(WorkOrderItemTravel dtPutObject) { WorkOrderItemTravel dbObject = await ct.WorkOrderItemTravel.SingleOrDefaultAsync(z => z.Id == dtPutObject.Id); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } // make a snapshot of the original for validation but update the original to preserve workflow WorkOrderItemTravel SnapshotOfOriginalDBObj = new WorkOrderItemTravel(); CopyObject.Copy(dbObject, SnapshotOfOriginalDBObj); CopyObject.Copy(dtPutObject, dbObject, "Id"); dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags); dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields); ct.Entry(dbObject).OriginalValues["Concurrency"] = dtPutObject.Concurrency; await TravelValidateAsync(dbObject, SnapshotOfOriginalDBObj); if (HasErrors) return null; try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await TravelExistsAsync(dtPutObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, AyaType.WorkOrderItemTravel, AyaEvent.Modified), ct); await TravelSearchIndexAsync(dbObject, false); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags); await HandlePotentialNotificationEvent(AyaEvent.Modified, dbObject, SnapshotOfOriginalDBObj); return dbObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task TravelDeleteAsync(long id) { WorkOrderItemTravel dbObject = await ct.WorkOrderItemTravel.SingleOrDefaultAsync(z => z.Id == id); TravelValidateCanDelete(dbObject); if (HasErrors) return false; ct.WorkOrderItemTravel.Remove(dbObject); await ct.SaveChangesAsync(); //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, AyaType.WorkOrderItemTravel, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct); await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, AyaType.WorkOrderItemTravel, ct); await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags); await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct); await HandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); return true; } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task TravelValidateAsync(WorkOrderItemTravel proposedObj, WorkOrderItemTravel currentObj) { //run validation and biz rules bool isNew = currentObj == null; if (proposedObj.WorkOrderItemId == 0) AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId"); else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId"); } //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemTravel.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);//note: this is passed only to add errors //validate custom fields CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields); } } private void TravelValidateCanDelete(WorkOrderItemTravel obj) { if (obj == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return; } //re-check rights here necessary due to traversal delete from Principle object if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItemTravel)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } ////////////////////////////////////////////// //INDEXING // private async Task TravelSearchIndexAsync(WorkOrderItemTravel obj, bool isNew) { //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, AyaType.WorkOrderItemTravel); SearchParams.AddText(obj.Notes).AddText(obj.Tags).AddCustomFields(obj.CustomFields); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task TravelGetSearchResultSummary(long id) { var obj = await ct.WorkOrderItemTravel.SingleOrDefaultAsync(z => z.Id == id); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.Notes).AddText(obj.Tags).AddCustomFields(obj.CustomFields); return SearchParams; } #endregion work order item LABOR level /* ██╗ ██╗███╗ ██╗██╗████████╗ ██║ ██║████╗ ██║██║╚══██╔══╝ ██║ ██║██╔██╗ ██║██║ ██║ ██║ ██║██║╚██╗██║██║ ██║ ╚██████╔╝██║ ╚████║██║ ██║ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝ */ #region WorkOrderItemUnit level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task UnitExistsAsync(long id) { return await ct.WorkOrderItemUnit.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task UnitCreateAsync(WorkOrderItemUnit newObject) { await UnitValidateAsync(newObject, null); if (HasErrors) return null; else { newObject.Tags = TagBiz.NormalizeTags(newObject.Tags); newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields); await ct.WorkOrderItemUnit.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, AyaType.WorkOrderItemUnit, AyaEvent.Created), ct); await UnitSearchIndexAsync(newObject, true); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null); await HandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task UnitGetAsync(long id, bool logTheGetEvent = true) { //Note: there could be rules checking here in future, i.e. can only get own workorder or something //if so, then need to implement AddError and in route handle Null return with Error check just like PUT route does now //https://docs.microsoft.com/en-us/ef/core/querying/related-data //docs say this will not query twice but will recognize the duplicate woitem bit which is required for multiple grandchild collections var ret = await ct.WorkOrderItemUnit .SingleOrDefaultAsync(z => z.Id == id); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, AyaType.WorkOrderItemUnit, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task UnitPutAsync(WorkOrderItemUnit dtPutObject) { WorkOrderItemUnit dbObject = await ct.WorkOrderItemUnit.SingleOrDefaultAsync(z => z.Id == dtPutObject.Id); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } // make a snapshot of the original for validation but update the original to preserve workflow WorkOrderItemUnit SnapshotOfOriginalDBObj = new WorkOrderItemUnit(); CopyObject.Copy(dbObject, SnapshotOfOriginalDBObj); CopyObject.Copy(dtPutObject, dbObject, "Id"); dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags); dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields); ct.Entry(dbObject).OriginalValues["Concurrency"] = dtPutObject.Concurrency; await UnitValidateAsync(dbObject, SnapshotOfOriginalDBObj); if (HasErrors) return null; try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await UnitExistsAsync(dtPutObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, AyaType.WorkOrderItemUnit, AyaEvent.Modified), ct); await UnitSearchIndexAsync(dbObject, false); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags); await HandlePotentialNotificationEvent(AyaEvent.Modified, dbObject, SnapshotOfOriginalDBObj); return dbObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task UnitDeleteAsync(long id) { WorkOrderItemUnit dbObject = await ct.WorkOrderItemUnit.SingleOrDefaultAsync(z => z.Id == id); UnitValidateCanDelete(dbObject); if (HasErrors) return false; ct.WorkOrderItemUnit.Remove(dbObject); await ct.SaveChangesAsync(); //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, AyaType.WorkOrderItemUnit, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct); await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, AyaType.WorkOrderItemUnit, ct); await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags); await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct); await HandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); return true; } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task UnitValidateAsync(WorkOrderItemUnit proposedObj, WorkOrderItemUnit currentObj) { //run validation and biz rules bool isNew = currentObj == null; if (proposedObj.WorkOrderItemId == 0) AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId"); else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId"); } //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemUnit.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);//note: this is passed only to add errors //validate custom fields CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields); } } private void UnitValidateCanDelete(WorkOrderItemUnit obj) { if (obj == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return; } //re-check rights here necessary due to traversal delete from Principle object if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItemUnit)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } ////////////////////////////////////////////// //INDEXING // private async Task UnitSearchIndexAsync(WorkOrderItemUnit obj, bool isNew) { //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, AyaType.WorkOrderItemUnit); SearchParams.AddText(obj.Notes).AddText(obj.Tags).AddCustomFields(obj.CustomFields); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task UnitGetSearchResultSummary(long id) { var obj = await ct.WorkOrderItemUnit.SingleOrDefaultAsync(z => z.Id == id); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.Notes).AddText(obj.Tags).AddCustomFields(obj.CustomFields); return SearchParams; } #endregion work order item LABOR level #region Utility public async Task GetWorkOrderGraphItem(AyaType ayaType, long id) { switch (ayaType) { case AyaType.WorkOrder: return await WorkOrderGetAsync(id, false) as ICoreBizObjectModel; case AyaType.WorkOrderItem: return await ItemGetAsync(id, false); case AyaType.WorkOrderItemExpense: return await ExpenseGetAsync(id, false); case AyaType.WorkOrderItemLabor: return await LaborGetAsync(id, false); case AyaType.WorkOrderItemLoan: return await LoanGetAsync(id, false); case AyaType.WorkOrderItemPart: return await PartGetAsync(id, false); case AyaType.WorkOrderItemPartRequest: return await PartRequestGetAsync(id, false); case AyaType.WorkOrderItemScheduledUser: return await ScheduledUserGetAsync(id, false); case AyaType.WorkOrderItemTask: return await TaskGetAsync(id, false); case AyaType.WorkOrderItemTravel: return await TravelGetAsync(id, false); case AyaType.WorkOrderItemUnit: return await UnitGetAsync(id, false); default: throw new System.ArgumentOutOfRangeException($"WorkOrder::GetWorkOrderGraphItem -> Invalid ayaType{ayaType}"); } } public async Task PutWorkOrderGraphItem(AyaType ayaType, ICoreBizObjectModel o) { ClearErrors(); switch (ayaType) { case AyaType.WorkOrder: if (o is WorkOrder) { WorkOrder dto = new WorkOrder(); CopyObject.Copy(o, dto); return await WorkOrderPutAsync((WorkOrder)dto); } return await WorkOrderPutAsync((WorkOrder)o) as ICoreBizObjectModel; case AyaType.WorkOrderItem: if (o is WorkOrderItem) { WorkOrderItem dto = new WorkOrderItem(); CopyObject.Copy(o, dto); return await ItemPutAsync((WorkOrderItem)dto); } return await ItemPutAsync((WorkOrderItem)o); case AyaType.WorkOrderItemExpense: return await ExpensePutAsync((WorkOrderItemExpense)o); case AyaType.WorkOrderItemLabor: return await LaborPutAsync((WorkOrderItemLabor)o); case AyaType.WorkOrderItemLoan: return await LoanPutAsync((WorkOrderItemLoan)o); case AyaType.WorkOrderItemPart: return await PartPutAsync((WorkOrderItemPart)o); case AyaType.WorkOrderItemPartRequest: return await PartRequestPutAsync((WorkOrderItemPartRequest)o); case AyaType.WorkOrderItemScheduledUser: return await ScheduledUserPutAsync((WorkOrderItemScheduledUser)o); case AyaType.WorkOrderItemTask: return await TaskPutAsync((WorkOrderItemTask)o); case AyaType.WorkOrderItemTravel: return await TravelPutAsync((WorkOrderItemTravel)o); case AyaType.WorkOrderItemUnit: return await UnitPutAsync((WorkOrderItemUnit)o); default: throw new System.ArgumentOutOfRangeException($"WorkOrder::PutWorkOrderGraphItem -> Invalid ayaType{ayaType}"); } } public async Task DeleteWorkOrderGraphItem(AyaType ayaType, long id) { switch (ayaType) { case AyaType.WorkOrder: return await WorkOrderDeleteAsync(id); case AyaType.WorkOrderItem: return await ItemDeleteAsync(id); case AyaType.WorkOrderItemExpense: return await ExpenseDeleteAsync(id); case AyaType.WorkOrderItemLabor: return await LaborDeleteAsync(id); case AyaType.WorkOrderItemLoan: return await LoanDeleteAsync(id); case AyaType.WorkOrderItemPart: return await PartDeleteAsync(id); case AyaType.WorkOrderItemPartRequest: return await PartRequestDeleteAsync(id); case AyaType.WorkOrderItemScheduledUser: return await ScheduledUserDeleteAsync(id); case AyaType.WorkOrderItemTask: return await TaskDeleteAsync(id); case AyaType.WorkOrderItemTravel: return await TravelDeleteAsync(id); case AyaType.WorkOrderItemUnit: return await UnitDeleteAsync(id); default: throw new System.ArgumentOutOfRangeException($"WorkOrder::GetWorkOrderGraphItem -> Invalid ayaType{ayaType}"); } } #endregion utility ///////////////////////////////////////////////////////////////////// }//eoc }//eons