diff --git a/server/AyaNova/biz/WorkOrderBiz.cs b/server/AyaNova/biz/WorkOrderBiz.cs index cda1d96e..66f01a43 100644 --- a/server/AyaNova/biz/WorkOrderBiz.cs +++ b/server/AyaNova/biz/WorkOrderBiz.cs @@ -8,11 +8,12 @@ using System.Linq; using System; using Newtonsoft.Json.Linq; using System.Collections.Generic; +using Newtonsoft.Json; namespace AyaNova.Biz { - internal class WorkOrderBiz : BizObject, IJobObject, ISearchAbleObject, INotifiableObject + internal class WorkOrderBiz : BizObject, IJobObject, ISearchAbleObject, IReportAbleObject, IExportAbleObject, IImportAbleObject, INotifiableObject { //Feature specific roles internal static AuthorizationRoles RolesAllowedToChangeSerial = AuthorizationRoles.BizAdminFull | AuthorizationRoles.DispatchFull | AuthorizationRoles.AccountingFull; @@ -260,48 +261,6 @@ namespace AyaNova.Biz - // //////////////////////////////////////////////////////////////////////////////////////////////// - // //GENERATE - // // - // internal async Task GenerateWorkOrderAsync(long customerId)//MIGRATE_OUTSTANDING will need more overloads as required and fleshing out later - // { - // WorkOrder newObject = new 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; - // } - // } - - - - //////////////////////////////////////////////////////////////////////////////////////////////// - //RESTART SERIAL - // - internal async Task RestartSerial(long newSerial) - { - - using (var command = ct.Database.GetDbConnection().CreateCommand()) - { - command.CommandText = $"alter table aworkorder alter column serial restart with {newSerial}"; - await ct.Database.OpenConnectionAsync(); - await command.ExecuteNonQueryAsync(); - await ct.Database.CloseConnectionAsync(); - } - await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, 0, BizType, AyaEvent.ResetSerial, newSerial.ToString()), ct); - return true; - } //////////////////////////////////////////////////////////////////////////////////////////////// @@ -383,6 +342,23 @@ namespace AyaNova.Biz //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"); @@ -420,6 +396,210 @@ namespace AyaNova.Biz { //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 @@ -2538,110 +2718,6 @@ namespace AyaNova.Biz - //////////////////////////////////////////////////////////////////////////////////////////////// - //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 - /////////////////////////////////////////////////////////////////////