Files
raven-client/ayanova/src/views/svc-workorder.vue
2021-05-24 21:54:46 +00:00

1896 lines
52 KiB
Vue

<template>
<div>
<gz-report-selector ref="reportSelector"></gz-report-selector>
<div v-if="formState.ready">
<gz-error :error-box-message="formState.errorBoxMessage"></gz-error>
<gz-alert :alert-message="obj.alertViz"></gz-alert>
<!-- SERVERERROR:{{ formState.serverError }} -->
<v-form ref="form">
<GzWoHeader
v-model="obj"
:form-key="formCustomTemplateKey"
:readonly="formState.readOnly"
:pvm="this"
data-cy="woHeader"
@change="setDirty()"
/>
<GzWoItems
v-model="obj"
:form-key="formCustomTemplateKey"
:readonly="formState.readOnly"
:pvm="this"
data-cy="woItems"
@change="setDirty()"
class="mt-16"
/>
</v-form>
</div>
<template v-if="!formState.ready">
<v-progress-circular
indeterminate
color="primary"
:size="60"
></v-progress-circular>
</template>
</div>
<!-- :reset-selections="resetSelections" -->
</template>
<script>
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
/* Xeslint-disable */
//
////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
import GzWoHeader from "../components/work-order-header.vue";
import GzWoItems from "../components/work-order-items.vue";
const FORM_KEY = "workorder-edit";
const API_BASE_URL = "workorder/";
const FORM_CUSTOM_TEMPLATE_KEY = "WorkOrder"; //<-- Should always be CoreBizObject AyaType name here where possible
export default {
components: {
GzWoHeader,
GzWoItems
},
async created() {
const vm = this;
try {
await initForm(vm);
//set base rights
vm.rights = window.$gz.role.getRights(window.$gz.type.WorkOrder);
window.$gz.eventBus.$on("menu-click", clickHandler);
//id 0 means create a new record don't load one
if (vm.$route.params.recordid != 0) {
//is there already an obj from a prior operation?
if (this.$route.params.obj) {
//yes, no need to fetch it
this.obj = this.$route.params.obj;
window.$gz.form.setFormState({
vm: vm,
loading: false
});
} else {
await vm.getDataFromApi(vm.$route.params.recordid); //let getdata handle loading
}
} else {
window.$gz.form.setFormState({
vm: vm,
loading: false
});
}
window.$gz.form.setFormState({
vm: vm,
dirty: false,
valid: true
});
//update which areas are available to user and force generate menu
updateRights(vm, true);
} catch (error) {
window.$gz.errorHandler.handleFormError(error, vm);
} finally {
vm.formState.ready = true;
}
},
async beforeRouteLeave(to, from, next) {
if (!this.formState.dirty || JUST_DELETED) {
next();
return;
}
if ((await window.$gz.dialog.confirmLeaveUnsaved()) === true) {
next();
} else {
next(false);
}
},
beforeDestroy() {
window.$gz.eventBus.$off("menu-click", clickHandler);
},
data() {
return {
formCustomTemplateKey: FORM_CUSTOM_TEMPLATE_KEY,
obj: {
id: 0,
concurrency: 0,
serial: 0,
notes: null,
wiki: null,
customFields: "{}",
tags: [],
customerId: null,
projectId: null,
contractId: null,
internalReferenceNumber: null,
customerReferenceNumber: null,
customerContactName: null,
fromQuoteId: null,
fromPMId: null,
fromCSRId: null,
serviceDate: null,
completeByDate: null,
durationToCompleted: "00:00:00",
invoiceNumber: null,
onsite: true,
customerSignature: null,
customerSignatureName: null,
customerSignatureCaptured: null,
techSignature: null,
techSignatureName: null,
techSignatureCaptured: null,
postAddress: null,
postCity: null,
postRegion: null,
postCountry: null,
postCode: null,
address: null,
city: null,
region: null,
country: null,
latitude: null,
longitude: null,
items: [],
states: [],
isDirty: true,
isLockedAtServer: false,
alertViz: null
},
formState: {
ready: false,
dirty: false,
valid: true,
readOnly: false,
loading: true,
errorBoxMessage: null,
appError: null,
serverError: {}
},
rights: window.$gz.role.defaultRightsObject(), //overall workorder rights, supersedes subrights
ayaType: window.$gz.type.WorkOrder,
currencyName: window.$gz.locale.getCurrencyName(),
timeZoneName: window.$gz.locale.getResolvedTimeZoneName(),
languageName: window.$gz.locale.getResolvedLanguage(),
hour12: window.$gz.locale.getHour12(),
// resetSelections: false,
selectLists: {
wostatus: [],
allowedwostatus: [],
woItemPriorities: [],
woItemStatus: [],
woItemTaskCompletionTypes: []
},
subRights: {
items: {
visible: true,
create: true,
change: true,
delete: true
},
states: {
visible: true,
create: true, //no change, only create it's write only
delete: true
},
labors: {
visible: true,
create: true,
change: true,
delete: true
},
parts: {
visible: true,
create: true,
change: true,
delete: true
},
partRequests: {
visible: true,
create: true,
change: true,
delete: true
},
expenses: {
visible: true,
create: true,
change: true,
delete: true
},
loans: {
visible: true,
create: true,
change: true,
delete: true
},
scheduledUsers: {
visible: true,
create: true,
change: true,
delete: true
},
tasks: {
visible: true,
create: true,
change: true,
delete: true
},
travels: {
visible: true,
create: true,
change: true,
delete: true
},
units: {
visible: true,
create: true,
change: true,
delete: true
},
outsideServices: {
visible: true,
create: true,
change: true,
delete: true
}
},
saveResult: {
fatal: false, //fatal error, further save is pointless, bail early and report
errors: null //contains error objects from save
},
lastGetContractId: -1, //note: -1 so that a new record updates
lastGetCustomerId: -1
};
},
//WATCHERS
watch: {
formState: {
handler: function(val) {
//,oldval is available here too if necessary
if (this.formState.loading) {
return;
}
//enable / disable save button
if (val.dirty && val.valid) {
window.$gz.eventBus.$emit("menu-enable-item", FORM_KEY + ":save");
} else {
window.$gz.eventBus.$emit("menu-disable-item", FORM_KEY + ":save");
}
//enable / disable duplicate / new button
if (!val.dirty && val.valid && !val.readOnly) {
window.$gz.eventBus.$emit(
"menu-enable-item",
FORM_KEY + ":duplicate"
);
window.$gz.eventBus.$emit("menu-enable-item", FORM_KEY + ":new");
} else {
window.$gz.eventBus.$emit(
"menu-disable-item",
FORM_KEY + ":duplicate"
);
window.$gz.eventBus.$emit("menu-disable-item", FORM_KEY + ":new");
}
},
deep: true
}
},
computed: {
currentState() {
//return actual status object from top level shell based on current state
//if state is unknown then it should return a placeholder dummy state showing an error condition or empty I guess
if (this.obj.states != null && this.obj.states.length > 0) {
//find it in the status collection
//and return here
const laststate = this.obj.states[this.obj.states.length - 1];
const found = this.selectLists.wostatus.find(
z => z.id == laststate.workOrderStatusId
);
if (found) {
return found;
}
}
//default
return {
id: 0,
name: "-",
active: true,
color: "#ffffff", //invisible
completed: false,
locked: false
};
}
},
methods: {
setDirty: function() {
this.formState.dirty = true;
},
canSave: function() {
return this.formState.valid && this.formState.dirty;
},
canDuplicate: function() {
return this.formState.valid && !this.formState.dirty;
},
ayaTypes: function() {
return window.$gz.type;
},
form() {
return window.$gz.form;
},
fieldValueChanged(ref) {
if (this.formState.ready && !this.formState.loading) {
window.$gz.form.fieldValueChanged(this, ref);
}
},
async getDataFromApi(recordId) {
const vm = this;
window.$gz.form.setFormState({
vm: vm,
loading: true
});
if (!recordId) {
throw new Error(FORM_KEY + "::getDataFromApi -> Missing recordID!");
}
const url = API_BASE_URL + recordId;
try {
window.$gz.form.deleteAllErrorBoxErrors(vm);
const res = await window.$gz.api.get(url);
if (res.error) {
//Not found?
if (res.error.code == "2010") {
window.$gz.form.handleObjectNotFound(vm);
}
vm.formState.serverError = res.error;
window.$gz.form.setErrorBoxErrors(vm);
} else {
vm.obj = res.data;
vm.lastGetContractId = vm.obj.contractId; //preserve for triggering full update if something changes it later
vm.lastGetCustomerId = vm.obj.customerId; //preserve for triggering full update if something changes it later
//assign opening UID's
vm.obj.items.forEach((z, index) => {
z.uid = index;
z.expenses.forEach((x, index) => (x.uid = index));
z.labors.forEach((x, index) => (x.uid = index));
z.loans.forEach((x, index) => (x.uid = index));
z.parts.forEach((x, index) => (x.uid = index));
z.partRequests.forEach((x, index) => (x.uid = index));
z.scheduledUsers.forEach((x, index) => (x.uid = index));
z.tasks.forEach((x, index) => (x.uid = index));
z.travels.forEach((x, index) => (x.uid = index));
z.units.forEach((x, index) => (x.uid = index));
z.outsideServices.forEach((x, index) => (x.uid = index));
});
//modify the menu as necessary
generateMenu(vm);
//update which areas are available to user
updateRights(vm);
//Update the form status
window.$gz.form.setFormState({
vm: vm,
dirty: false,
valid: true,
loading: false
});
}
} catch (error) {
window.$gz.errorHandler.handleFormError(error, vm);
} finally {
window.$gz.form.setFormState({
vm: vm,
loading: false
});
}
},
// async submitNewContract() {
// //save new contract route, this only ever gets called from a clean wo with no dirty edits so just save the contract and reset the wo from the result
// let res = await window.$gz.api.post(
// `${API_BASE_URL}set-contract/${this.obj.id}`,
// { newContractId: this.obj.contractId }
// );
// if (res.error) {
// this.formState.serverError = res.error;
// window.$gz.form.setErrorBoxErrors(this);
// } else {
// this.obj = res.data;
// window.$gz.form.setFormState({
// vm: this,
// dirty: false,
// valid: true
// });
// }
// },
async submit() {
const vm = this;
if (vm.canSave == false) {
return;
}
try {
window.$gz.form.setFormState({
vm: vm,
loading: true
});
//clear any errors vm might be around from previous submit
window.$gz.form.deleteAllErrorBoxErrors(vm);
//#######################################################
// ███████╗ █████╗ ██╗ ██╗███████╗
// ██╔════╝██╔══██╗██║ ██║██╔════╝
// ███████╗███████║██║ ██║█████╗
// ╚════██║██╔══██║╚██╗ ██╔╝██╔══╝
// ███████║██║ ██║ ╚████╔╝ ███████╗
// ╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝
//########################################################
const isPost = vm.obj.id == 0;
//some ops require a full refresh like ones that trigger contract changes
let forceFullRefresh = false;
//reset error object
this.saveResult.fatal = false;
this.saveResult.errors = null;
//UNSAVED HEADER MUST BE FIRST
//(otherwise there's nothing to hang the other things off of)
let headerSaved = false;
if (this.obj.concurrency == 0) {
await saveHeader(vm);
headerSaved = true;
}
//LOCKED? State must be saved first then (assuming it unlocks)
let stateSaved = false;
if (this.obj.isLockedAtServer) {
await saveState(vm);
if (!this.saveResult.fatal) {
stateSaved = true;
//update which areas are available to user
//which may have changed due to state being saved (saveState sets the current islocked value)
updateRights(vm);
}
}
//HEADER
if (!this.saveResult.fatal && !headerSaved) {
await saveHeader(vm);
}
//WOITEMS
if (!this.saveResult.fatal) {
//first sort items into sequence order so that the errors line up with the display
this.obj.items.sort((a, b) => a.sequence - b.sequence);
//This saves all bottom level collections as well
await saveItems(vm);
}
//### STATE last normally
//in case it locks or is completed
if (!this.saveResult.fatal && !stateSaved) {
await saveState(vm);
if (!this.saveResult.fatal) {
updateRights(vm);
}
}
//## ALL PARTIAL UPDATES COMPLETED
//handle errors
if (this.saveResult.errors != null) {
//# FAIL ROUTE
const processedErrors = formErrorFromSaveResult(this);
vm.formState.serverError = processedErrors; //this is the bit that triggers field and row errors to display as they pickup from this setting
window.$gz.form.setErrorBoxErrors(vm); //set generalerror errors in error box at top, not related to form field errors which happen alternatively based on formState.serverError which is confusing
} else {
//# SUCCESS ROUTE
if (isPost) {
//nav to id'd url
//note that an isPost will never be here if there is a fatal error so this is safe to do
this.$router.push({
name: "workorder-edit",
params: {
recordid: vm.obj.id
// ,obj: vm.obj // Do NOT Pass data object to new form as normal because for a workorder, it's not a full and complete record at this end
}
});
} else {
//check if full refresh is necessary
if (
vm.obj.contractId != vm.lastGetContractId ||
vm.obj.customerId != vm.lastGetCustomerId
) {
//there may be others which is why I'm doing it this way with the extra variable
forceFullRefresh = true;
}
if (forceFullRefresh) {
await vm.getDataFromApi(vm.$route.params.recordid);
} else {
window.$gz.form.setFormState({
vm: vm,
dirty: false,
valid: true
});
}
}
}
} catch (ex) {
window.$gz.errorHandler.handleFormError(ex, vm);
} finally {
window.$gz.form.setFormState({
vm: vm,
loading: false
});
}
},
async remove() {
let vm = this;
try {
let dialogResult = await window.$gz.dialog.confirmDelete();
if (dialogResult != true) {
return;
}
//do the delete
window.$gz.form.setFormState({
vm: vm,
loading: true
});
//No need to delete a new record, just abandon it...
if (vm.$route.params.recordid == 0) {
//this should not get offered for delete but to be safe and clear just in case:
JUST_DELETED = true;
// navigate backwards
vm.$router.go(-1);
} else {
let url = API_BASE_URL + vm.$route.params.recordid;
window.$gz.form.deleteAllErrorBoxErrors(vm);
let res = await window.$gz.api.remove(url);
if (res.error) {
vm.formState.serverError = res.error;
window.$gz.form.setErrorBoxErrors(vm);
} else {
//workaround to prevent warning about leaving dirty record
//For some reason I couldn't just reset isdirty in formstate
JUST_DELETED = true;
// navigate backwards
vm.$router.go(-1);
}
}
} catch (error) {
//Update the form status
window.$gz.form.setFormState({
vm: vm,
loading: false
});
window.$gz.errorHandler.handleFormError(error, vm);
}
},
async duplicate() {
let vm = this;
if (!vm.canDuplicate || vm.$route.params.recordid == 0) {
return;
}
window.$gz.form.setFormState({
vm: vm,
loading: true
});
let url = API_BASE_URL + "duplicate/" + vm.$route.params.recordid;
try {
window.$gz.form.deleteAllErrorBoxErrors(vm);
let res = await window.$gz.api.upsert(url);
if (res.error) {
vm.formState.serverError = res.error;
window.$gz.form.setErrorBoxErrors(vm);
} else {
//Navigate to new record
this.$router.push({
name: "workorder-edit",
params: {
recordid: res.data.id,
obj: res.data // Pass data object to new form
}
});
}
} catch (ex) {
window.$gz.errorHandler.handleFormError(ex, vm);
} finally {
window.$gz.form.setFormState({
vm: vm,
loading: false
});
}
}
//end methods
}
};
//########################################## SAVE METHODS ##############################################
/////////////////////////////
// HEADER
//
async function saveHeader(vm) {
if (!vm.obj.isDirty) {
return;
}
const isPost = vm.obj.id == 0;
//the purpose of this is to remove the child collections so only the header itself is submitted
//this was the cleanest way I could find to accomplish this
//https://stackoverflow.com/a/58206483/8939
const { items: removedKey1, states: removedKey2, ...headerOnly } = vm.obj;
//remove *Viz keys so they don't generate traffic
headerOnly.alertViz = undefined;
headerOnly.projectViz = undefined;
headerOnly.contractViz = undefined;
headerOnly.customerViz = undefined;
let res = await window.$gz.api.upsert(`${API_BASE_URL}`, headerOnly);
if (res.error) {
handleSaveError(vm, { fatal: true, error: res.error });
} else {
//update any server changed fields
vm.obj.concurrency = res.data.concurrency;
vm.obj.isDirty = false;
vm.obj.customerId = res.data.customerId;
vm.obj.contractId = res.data.contractId; //may or may not have changed at server, this will ensure entire ui gets updated if it has as all prices may have changed and other stuff
//repopulate *viz fields from return value
vm.obj.alertViz = res.data.alertViz;
vm.obj.projectViz = res.data.projectViz;
vm.obj.contractViz = res.data.contractViz;
vm.obj.customerViz = res.data.customerViz;
if (isPost) {
vm.obj.id = res.data.id;
vm.obj.serial = res.data.serial;
//walk all unsaved direct children and set the workorder id so they can save
vm.obj.states.forEach(z => (z.workOrderId = vm.obj.id));
vm.obj.items.forEach(z => (z.workOrderId = vm.obj.id));
}
}
}
/////////////////////////////
// STATES
//
async function saveState(vm) {
//CHANGED?
let totalItems = vm.obj.states.length;
if (totalItems == 0) {
return;
}
for (let i = 0; i < totalItems; i++) {
let o = vm.obj.states[i];
if (o.concurrency == null) {
//it's new so save it
let res = await window.$gz.api.upsert(`${API_BASE_URL}states`, o);
if (res.error) {
handleSaveError(vm, { error: res.error });
} else {
vm.obj.states[i] = res.data;
//set locked status of entire wo now
vm.obj.isLockedAtServer = vm.currentState.locked;
}
}
}
}
/////////////////////////////
// ITEMS
//
async function deleteItems(vm) {
//walk the array backwards as items may or may not be spliced out
for (var i = vm.obj.items.length - 1; i >= 0; i--) {
const d = vm.obj.items[i];
if (!d.deleted) {
continue;
}
let res = await window.$gz.api.remove(`${API_BASE_URL}items/${d.id}`);
if (res.error) {
handleSaveError(vm, { error: res.error, itemUid: d.uid });
} else {
vm.obj.items.splice(i, 1);
}
}
return;
}
async function saveItems(vm) {
//DELETE FLAGGED WOITEMS FIRST
await deleteItems(vm);
if (vm.saveResult.fatal) {
return;
}
//SAVE WOITEMS
for (let i = 0; i < vm.obj.items.length; i++) {
//get copy of item without child collections for independant submit
const {
expenses: removedKey1,
labors: removedKey2,
loans: removedKey3,
parts: removedKey4,
partRequests: removedKey5,
scheduledUsers: removedKey6,
tasks: removedKey7,
travels: removedKey8,
units: removedKey9,
outsideServices: removedKey10,
...o
} = vm.obj.items[i];
if (o.isDirty) {
const isPost = o.id == 0;
let res = await window.$gz.api.upsert(`${API_BASE_URL}items`, o);
if (res.error) {
handleSaveError(vm, { error: res.error, itemUid: o.uid });
if (isPost) {
//a post error precludes further operations on this item below
//however, an update error doesn't necessarily because it's still a existing workorder item
//so it's children can probably be updated and we want that
continue;
}
} else {
//no error so update any server changed fields
//put fields
vm.obj.items[i].concurrency = res.data.concurrency;
vm.obj.items[i].isDirty = false;
//fields to update if post
if (isPost) {
vm.obj.items[i].id = res.data.id;
vm.obj.items[i].workorderId = res.data.workorderId;
//walk all unsaved children and set the workorder id so they can save
vm.obj.items[i].expenses.forEach(
z => (z.workorderItemId = vm.obj.items[i].id)
);
vm.obj.items[i].scheduledUsers.forEach(
z => (z.workorderItemId = vm.obj.items[i].id)
);
vm.obj.items[i].labors.forEach(
z => (z.workorderItemId = vm.obj.items[i].id)
);
vm.obj.items[i].travels.forEach(
z => (z.workorderItemId = vm.obj.items[i].id)
);
//todo: other grandchildren
}
}
} else {
// Item not dirty skipping save
}
//------
//save grandchildren
if (!vm.saveResult.fatal) {
await saveExpenses(vm, i);
}
if (!vm.saveResult.fatal) {
await saveScheduledUsers(vm, i);
}
if (!vm.saveResult.fatal) {
await saveLabors(vm, i);
}
if (!vm.saveResult.fatal) {
await saveTravels(vm, i);
}
//todo: other grandchildren
//------
}
}
//####################################################################################
//## GRANDCHILDREN
/////////////////////////////
// EXPENSES
//
async function deleteExpenses(vm, woItemIndex) {
//walk the array backwards as items may be spliced out
for (var i = vm.obj.items[woItemIndex].expenses.length - 1; i >= 0; i--) {
const d = vm.obj.items[woItemIndex].expenses[i];
if (!d.deleted) {
continue;
}
let res = await window.$gz.api.remove(
`${API_BASE_URL}items/expenses/${d.id}`
);
if (res.error) {
handleSaveError(vm, {
error: res.error,
itemUid: vm.obj.items[woItemIndex].uid,
childKey: "expenses",
childUid: d.uid
});
} else {
vm.obj.items[woItemIndex].expenses.splice(i, 1);
}
}
//----
return;
}
async function saveExpenses(vm, woItemIndex) {
//DELETE FLAGGED ITEMS FIRST
await deleteExpenses(vm, woItemIndex);
if (vm.saveResult.fatal) {
return;
}
for (let i = 0; i < vm.obj.items[woItemIndex].expenses.length; i++) {
let o = vm.obj.items[woItemIndex].expenses[i];
if (o.isDirty) {
const isPost = o.id == 0;
let res = await window.$gz.api.upsert(`${API_BASE_URL}items/expenses`, o);
if (res.error) {
handleSaveError(vm, {
error: res.error,
itemUid: vm.obj.items[woItemIndex].uid,
childKey: "expenses",
childUid: o.uid
});
} else {
//Server will update fields on put or post for most workorder graph objecs so need to update entire object here
vm.obj.items[woItemIndex].expenses.splice(i, 1, res.data); //vue needs the splice rather than just setting the value in order to trigger reactivity or else the UI won't update
}
}
}
return; //made it
}
/////////////////////////////
// SCHEDULED USERS
//
async function saveScheduledUsers(vm, woItemIndex) {
//DELETE FLAGGED ITEMS FIRST
await deleteScheduledUsers(vm, woItemIndex);
if (vm.saveResult.fatal) {
return;
}
for (let i = 0; i < vm.obj.items[woItemIndex].scheduledUsers.length; i++) {
let o = vm.obj.items[woItemIndex].scheduledUsers[i];
if (o.isDirty) {
const isPost = o.id == 0;
let res = await window.$gz.api.upsert(
`${API_BASE_URL}items/scheduledusers`,
o
);
if (res.error) {
handleSaveError(vm, {
error: res.error,
itemUid: vm.obj.items[woItemIndex].uid,
childKey: "scheduledUsers",
childUid: o.uid
});
} else {
//Server will update fields on put or post for most workorder graph objecs so need to update entire object here
vm.obj.items[woItemIndex].scheduledUsers.splice(i, 1, res.data); //vue needs the splice rather than just setting the value in order to trigger reactivity or else the UI won't update
}
}
}
return; //made it
}
async function deleteScheduledUsers(vm, woItemIndex) {
//walk the array backwards as items may be spliced out
for (
var i = vm.obj.items[woItemIndex].scheduledUsers.length - 1;
i >= 0;
i--
) {
const d = vm.obj.items[woItemIndex].scheduledUsers[i];
if (!d.deleted) {
continue;
}
let res = await window.$gz.api.remove(
`${API_BASE_URL}items/scheduledusers/${d.id}`
);
if (res.error) {
handleSaveError(vm, {
error: res.error,
itemUid: vm.obj.items[woItemIndex].uid,
childKey: "scheduledUsers",
childUid: d.uid
});
} else {
vm.obj.items[woItemIndex].scheduledUsers.splice(i, 1);
}
}
//----
return;
}
/////////////////////////////
// LABOR
//
async function saveLabors(vm, woItemIndex) {
//DELETE FLAGGED ITEMS FIRST
await deleteLabors(vm, woItemIndex);
if (vm.saveResult.fatal) {
return;
}
for (let i = 0; i < vm.obj.items[woItemIndex].labors.length; i++) {
let o = vm.obj.items[woItemIndex].labors[i];
if (o.isDirty) {
const res = await window.$gz.api.upsert(`${API_BASE_URL}items/labors`, o);
if (res.error) {
handleSaveError(vm, {
error: res.error,
itemUid: vm.obj.items[woItemIndex].uid,
childKey: "labors",
childUid: o.uid
});
} else {
//Server will update fields on put or post for most workorder graph objecs so need to update entire object here
vm.obj.items[woItemIndex].labors.splice(i, 1, res.data); //vue needs the splice rather than just setting the value in order to trigger reactivity or else the UI won't update
}
}
}
return;
}
async function deleteLabors(vm, woItemIndex) {
//walk the array backwards as items may be spliced out
for (var i = vm.obj.items[woItemIndex].labors.length - 1; i >= 0; i--) {
const d = vm.obj.items[woItemIndex].labors[i];
if (!d.deleted) {
continue;
}
let res = await window.$gz.api.remove(
`${API_BASE_URL}items/labors/${d.id}`
);
if (res.error) {
handleSaveError(vm, {
error: res.error,
itemUid: vm.obj.items[woItemIndex].uid,
childKey: "labors",
childUid: d.uid
});
} else {
vm.obj.items[woItemIndex].labors.splice(i, 1);
}
}
//----
return;
}
/////////////////////////////
// TRAVEL
//
async function saveTravels(vm, woItemIndex) {
//DELETE FLAGGED ITEMS FIRST
await deleteTravels(vm, woItemIndex);
if (vm.saveResult.fatal) {
return;
}
for (let i = 0; i < vm.obj.items[woItemIndex].travels.length; i++) {
let o = vm.obj.items[woItemIndex].travels[i];
if (o.isDirty) {
const res = await window.$gz.api.upsert(
`${API_BASE_URL}items/travels`,
o
);
if (res.error) {
handleSaveError(vm, {
error: res.error,
itemUid: vm.obj.items[woItemIndex].uid,
childKey: "travels",
childUid: o.uid
});
} else {
//Server will update fields on put or post for most workorder graph objecs so need to update entire object here
vm.obj.items[woItemIndex].travels.splice(i, 1, res.data); //vue needs the splice rather than just setting the value in order to trigger reactivity or else the UI won't update
}
}
}
return;
}
async function deleteTravels(vm, woItemIndex) {
//walk the array backwards as items may be spliced out
for (var i = vm.obj.items[woItemIndex].travels.length - 1; i >= 0; i--) {
const d = vm.obj.items[woItemIndex].travels[i];
if (!d.deleted) {
continue;
}
let res = await window.$gz.api.remove(
`${API_BASE_URL}items/travels/${d.id}`
);
if (res.error) {
handleSaveError(vm, {
error: res.error,
itemUid: vm.obj.items[woItemIndex].uid,
childKey: "travels",
childUid: d.uid
});
} else {
vm.obj.items[woItemIndex].travels.splice(i, 1);
}
}
//----
return;
}
//todo: other grandchildren
//######################################### UTILITY METHODS ###########################################
function handleSaveError(vm, e) {
//### NOTE: Nothing using FATAL yet, but if necessary here is where it should be determined and set
if (vm.saveResult.errors == null) {
vm.saveResult.errors = [];
}
vm.saveResult.errors.push(e);
}
function errorTargetFromSaveResult(vm, r, t) {
//no particular target
//or not a woitem tree error (header)
if (r.itemUid == null || t == null || t == "generalerror") {
return t;
}
let woitemindex = vm.obj.items.findIndex(z => z.uid == r.itemUid);
if (woitemindex == -1) {
return null;
}
if (r.childKey == null || r.childUid == null) {
return `Items[${woitemindex}].${t}`;
}
let childindex = vm.obj.items[woitemindex][r.childKey].findIndex(
z => z.uid == r.childUid
);
if (childindex == -1) {
return null;
}
return `Items[${woitemindex}].${r.childKey}[${childindex}].${t}`;
}
function formErrorFromSaveResult(vm) {
/**
* convention:
* {
error: res.error,
itemUid: item.uid, //uid of item none means it's a header error
childKey: "scheduledUsers",
childUid: i //uid of child item will be located later as index
}
*
*/
//Note: re code 2200 hard coded here below -- if for some reason it becomes necessary there is the idea of making an envelope error to contain variety of errors,
//but so far I think they are all server errors (that go in the error box) or validation errors that work here
let ret = {
code: "2200",
details: [],
message: "ErrorAPI2200"
};
//console.log("formErrorFromSaveResult - input is:", vm.saveResult.errors);
vm.saveResult.errors.forEach(z => {
//console.log("z", z);
//{error:{code:xx,message:xx,target:xx},fatal:true}
if (z.error.details != null) {
z.error.details.forEach(x => {
let target = errorTargetFromSaveResult(vm, z, x.target);
if (target != null) {
ret.details.push({
message: x.message,
error: x.error,
target: target
});
}
});
} else {
//only one error so just return it
//console.log("single error path:", z.error);
ret = z.error;
//ret.details.push(z.error);
}
});
//console.log("Returning ret=:", ret);
return ret;
}
/////////////////////////////
//
//
function updateRights(vm, forceGenerateMenu) {
//determine rights to each which sections are hidden due to form customized out or rights / roles
const readOnlyBefore = vm.formState.readOnly;
if (vm.obj.isLockedAtServer) {
//locked is always read only (with state exception for sufficient roles)
vm.formState.readOnly = true;
} else {
//state may have changed so update readOnly
vm.formState.readOnly = !vm.rights.change;
}
if (readOnlyBefore != vm.formState.readOnly || forceGenerateMenu === true) {
generateMenu(vm);
}
}
/////////////////////////////
//
//
async function clickHandler(menuItem) {
if (!menuItem) {
return;
}
let m = window.$gz.menu.parseMenuItem(menuItem);
if (m.owner == FORM_KEY && !m.disabled) {
switch (m.key) {
case "save":
m.vm.submit();
break;
case "delete":
m.vm.remove();
break;
case "new":
m.vm.$router.push({
name: "workorder-edit",
params: { recordid: 0, new: true }
});
break;
case "duplicate":
m.vm.duplicate();
break;
case "report":
if (m.id != null) {
//last report selected is in m.id
m.vm.$router.push({
name: "ay-report",
params: { recordid: m.id, ayatype: window.$gz.type.WorkOrder }
});
} else {
//general report selector chosen
let res = await m.vm.$refs.reportSelector.open({
AType: window.$gz.type.WorkOrder,
selectedRowIds: [m.vm.obj.id]
});
//if null for no selection
//just bail out
if (res == null) {
return;
}
//persist last report selected
window.$gz.form.setLastReport(FORM_KEY, res);
//Now open the report viewer...
m.vm.$router.push({
name: "ay-report",
params: { recordid: res.id, ayatype: window.$gz.type.WorkOrder }
});
}
break;
case "statuslist":
m.vm.$router.push({
name: "svc-work-order-status"
});
break;
case "itemstatuslist":
m.vm.$router.push({
name: "svc-work-order-item-status"
});
break;
case "prioritylist":
m.vm.$router.push({
name: "svc-work-order-item-priorities"
});
break;
default:
window.$gz.eventBus.$emit(
"notify-warning",
FORM_KEY + "::context click: [" + m.key + "]"
);
}
}
}
//////////////////////
//
//
function generateMenu(vm) {
let menuOptions = {
isMain: false,
readOnly: vm.formState.readOnly,
icon: "$ayiTools",
title: "WorkOrder",
helpUrl: "svc-workorder",
formData: {
ayaType: window.$gz.type.WorkOrder,
recordId: vm.$route.params.recordid,
formCustomTemplateKey: FORM_CUSTOM_TEMPLATE_KEY,
recordName: vm.obj.serial == 0 ? null : vm.obj.serial.toString()
},
menuItems: []
};
if (vm.rights.change) {
menuOptions.menuItems.push({
title: "Save",
icon: "$ayiSave",
surface: true,
key: FORM_KEY + ":save",
vm: vm
});
}
if (vm.rights.delete && vm.$route.params.recordid != 0) {
menuOptions.menuItems.push({
title: "Delete",
icon: "$ayiTrashAlt",
surface: false,
key: FORM_KEY + ":delete",
vm: vm
});
}
//REPORTS
//Report not Print, print is a further option
menuOptions.menuItems.push({
title: "Report",
icon: "$ayiFileAlt",
key: FORM_KEY + ":report",
vm: vm
});
//get last report selected
let lastReport = window.$gz.form.getLastReport(FORM_KEY);
if (lastReport != null) {
menuOptions.menuItems.push({
title: lastReport.name,
icon: "$ayiFileAlt",
key: FORM_KEY + ":report:" + lastReport.id,
vm: vm
});
}
if (vm.rights.change) {
menuOptions.menuItems.push({
title: "New",
icon: "$ayiPlus",
key: FORM_KEY + ":new",
vm: vm
});
}
if (vm.rights.change) {
menuOptions.menuItems.push({
title: "Duplicate",
icon: "$ayiClone",
key: FORM_KEY + ":duplicate",
vm: vm
});
}
//--- /show all ---
menuOptions.menuItems.push({ divider: true, inset: false });
menuOptions.menuItems.push({
title: "WorkOrderStatusList",
icon: "$ayiFlag",
key: FORM_KEY + ":statuslist",
vm: vm
});
menuOptions.menuItems.push({
title: "WorkOrderItemStatusList",
icon: "$ayiCircle",
key: FORM_KEY + ":itemstatuslist",
vm: vm
});
menuOptions.menuItems.push({
title: "WorkOrderItemPriorityList",
icon: "$ayiFireAlt",
key: FORM_KEY + ":prioritylist",
vm: vm
});
menuOptions.menuItems.push({ divider: true, inset: false });
window.$gz.eventBus.$emit("menu-change", menuOptions);
}
let JUST_DELETED = false;
/////////////////////////////////
//
//
async function initForm(vm) {
await fetchTranslatedText(vm);
await window.$gz.formCustomTemplate.get(FORM_CUSTOM_TEMPLATE_KEY, vm);
await populateSelectionLists(vm);
}
//////////////////////////////////////////////////////////
//
// Ensures UI translated text is available
//
async function fetchTranslatedText(vm) {
await window.$gz.translation.cacheTranslations([
"WorkOrder",
"Customer",
"WorkOrderSerialNumber",
"WorkOrderSummary",
"WorkOrderCloseByDate",
"Contract",
"Project",
"WorkOrderInvoiceNumber",
"WorkOrderServiceDate",
"WorkOrderCustomerContactName",
"WorkOrderCustomerReferenceNumber",
"WorkOrderInternalReferenceNumber",
"WorkOrderOnsite",
"NewStatus",
"WorkOrderStatus",
"WorkOrderCustom1",
"WorkOrderCustom2",
"WorkOrderCustom3",
"WorkOrderCustom4",
"WorkOrderCustom5",
"WorkOrderCustom6",
"WorkOrderCustom7",
"WorkOrderCustom8",
"WorkOrderCustom9",
"WorkOrderCustom10",
"WorkOrderCustom11",
"WorkOrderCustom12",
"WorkOrderCustom13",
"WorkOrderCustom14",
"WorkOrderCustom15",
"WorkOrderCustom16",
"WorkOrderItemList",
"WorkOrderItemTechNotes",
"WorkOrderItemScheduledUserList",
"WorkOrderItemSummary",
"WorkOrderItemWorkOrderStatusID",
"WorkOrderItemRequestDate",
"WorkOrderItemPriorityID",
"WorkOrderItemWarrantyService",
"WorkOrderItemScheduledUserStartDate",
"WorkOrderItemScheduledUserStopDate",
"WorkOrderItemScheduledUserEstimatedQuantity",
"WorkOrderItemScheduledUserUserID",
"WorkOrderItemScheduledUserServiceRateID",
"Sequence",
"WorkOrderItemUnitList",
"AddressTypePhysical",
"AddressTypePostal",
"AddressCopyToPostal",
"AddressCopyToPhysical",
"Address",
"AddressPostalDeliveryAddress",
"AddressPostalCity",
"AddressPostalStateProv",
"AddressPostalCountry",
"AddressPostalPostal",
"AddressDeliveryAddress",
"AddressCity",
"AddressStateProv",
"AddressCountry",
"AddressLatitude",
"AddressLongitude",
"SelectAlternateAddress",
"WorkOrderItemStatusList",
"WorkOrderStatusList",
"WorkOrderItemPriorityList",
"WorkOrderItemExpenseChargeAmount",
"WorkOrderItemExpenseChargeTaxCodeID",
"WorkOrderItemExpenseChargeToCustomer",
"WorkOrderItemExpenseDescription",
"WorkOrderItemExpenseList",
"WorkOrderItemExpenseName",
"WorkOrderItemExpenseReimburseUser",
"WorkOrderItemExpenseTaxPaid",
"WorkOrderItemExpenseTotalCost",
"WorkOrderItemExpenseUserID",
"WorkOrderItemLaborList",
"WorkOrderItemLaborServiceStartDate",
"WorkOrderItemLaborServiceStopDate",
"WorkOrderItemLaborServiceRateQuantity",
"WorkOrderItemLaborServiceRateID",
"WorkOrderItemLaborServiceDetails",
"WorkOrderItemLaborUserID",
"WorkOrderItemLaborNoChargeQuantity",
"WorkOrderItemLaborTaxRateSaleID",
"WorkOrderItemLaborPrice",
"WorkOrderItemTravelList",
"WorkOrderItemTravelStartDate",
"WorkOrderItemTravelStopDate",
"WorkOrderItemTravelRateQuantity",
"WorkOrderItemTravelServiceRateID",
"WorkOrderItemTravelDetails",
"WorkOrderItemTravelUserID",
"WorkOrderItemTravelNoChargeQuantity",
"WorkOrderItemTravelTaxRateSaleID",
"WorkOrderItemTravelPrice",
"SaveRecordToProceed",
"Cost",
"ListPrice",
"NetPrice",
"Price",
"PriceOverride",
"Tax",
"TaxAAmt",
"TaxBAmt",
"LineTotal",
"UnitOfMeasure"
]);
}
//////////////////////
//
//
async function populateSelectionLists(vm) {
let res = await window.$gz.api.get("work-order-status/list");
if (res.error) {
vm.formState.serverError = res.error;
window.$gz.form.setErrorBoxErrors(vm);
} else {
vm.selectLists.wostatus = res.data.all;
vm.selectLists.allowedwostatus = res.data.allowed;
}
res = await window.$gz.api.get("work-order-item-status/list");
if (res.error) {
vm.formState.serverError = res.error;
window.$gz.form.setErrorBoxErrors(vm);
} else {
vm.selectLists.woItemStatus = res.data;
}
res = await window.$gz.api.get("work-order-item-priority/list");
if (res.error) {
vm.formState.serverError = res.error;
window.$gz.form.setErrorBoxErrors(vm);
} else {
vm.selectLists.woItemPriorities = res.data;
}
await window.$gz.enums.fetchEnumList("WorkorderItemTaskCompletionType"); //prefetch
vm.selectLists.woItemTaskCompletionTypes = window.$gz.enums.getSelectionList(
"WorkorderItemTaskCompletionType"
);
//---------
/*
{
"data": {
"id": 10,
"concurrency": 7981529,
"serial": 10,
"notes": "Consequatur reprehenderit eveniet molestiae molestiae molestiae autem.",
"wiki": null,
"customFields": null,
"tags": [
"brown",
"red",
"zone8",
"zone9"
],
"customerId": 43,
"projectId": null,
"projectViz": null,
"internalReferenceNumber": "irf-6149",
"customerReferenceNumber": "crf-0272",
"customerContactName": "contact name here",
"fromQuoteId": null,
"fromPMId": null,
"fromCSRId": null,
"serviceDate": "2021-02-01T17:00:00Z",
"completeByDate": "2021-02-06T17:00:00Z",
"durationToCompleted": "00:00:00",
"invoiceNumber": null,
"customerSignature": null,
"customerSignatureName": null,
"customerSignatureCaptured": null,
"techSignature": null,
"techSignatureName": null,
"techSignatureCaptured": null,
"onsite": false,
"contractId": null,
"contractViz": "-",
"postAddress": "6304 Eduardo Lights",
"postCity": "Schoenfort",
"postRegion": "South Dakota",
"postCountry": "Liberia",
"postCode": "03315",
"address": "750 Darrell Cliff",
"city": "Armstrongview",
"region": "Illinois",
"country": "Israel",
"latitude": 9.8268,
"longitude": 34.3788,
"items": [
{
"id": 20,
"concurrency": 7981529,
"notes": "itemnotes - 0 ",
"wiki": null,
"customFields": null,
"tags": [],
"workOrderId": 10,
"techNotes": "technotes - 0",
"workorderItemStatusId": 1,
"workorderItemPriorityId": 5,
"requestDate": "2021-02-01T17:00:00Z",
"warrantyService": false,
"sequence": 1,
"isDirty": false,
"expenses": [
{
"id": 39,
"concurrency": 7981529,
"description": null,
"name": "Small Soft Pants",
"totalCost": 5.459658245770105,
"chargeAmount": 6.551589894924126,
"taxPaid": 0,
"chargeTaxCodeId": 2,
"chargeTaxCodeViz": "Goods only",
"reimburseUser": true,
"userId": 43,
"userViz": "Carolyne Watsica",
"chargeToCustomer": true,
"isDirty": false,
"workOrderItemId": 20
},
{
"id": 40,
"concurrency": 7981529,
"description": null,
"name": "Tasty Wooden Salad",
"totalCost": 10.91931649154021,
"chargeAmount": 12.011248140694232,
"taxPaid": 0,
"chargeTaxCodeId": 2,
"chargeTaxCodeViz": "Goods only",
"reimburseUser": true,
"userId": 25,
"userViz": "Dereck Romaguera",
"chargeToCustomer": true,
"isDirty": false,
"workOrderItemId": 20
}
],
"labors": [
{
"id": 39,
"concurrency": 7981529,
"userId": 25,
"userViz": "Dereck Romaguera",
"serviceStartDate": "2021-02-01T17:00:00Z",
"serviceStopDate": "2021-02-01T18:00:00Z",
"serviceRateId": 6,
"serviceRateViz": "Purple TV",
"serviceDetails": "Error autem quaerat voluptatem.",
"serviceRateQuantity": 1,
"noChargeQuantity": 0,
"serviceBankId": null,
"taxCodeSaleId": null,
"taxCodeSaleViz": null,
"cost": 0,
"listPrice": 0,
"price": 0,
"taxName": null,
"taxAPct": 0,
"taxBPct": 0,
"taxOnTax": false,
"taxAViz": 0,
"taxBViz": 0,
"lineTotalViz": 0,
"isDirty": false,
"workOrderItemId": 20
},
{
"id": 40,
"concurrency": 7981529,
"userId": 22,
"userViz": "Georgette Dooley - CustomerLimited",
"serviceStartDate": "2021-02-01T17:00:00Z",
"serviceStopDate": "2021-02-01T18:00:00Z",
"serviceRateId": 2,
"serviceRateViz": "Silver service rate",
"serviceDetails": "Ipsam id officiis non.",
"serviceRateQuantity": 2,
"noChargeQuantity": 0,
"serviceBankId": null,
"taxCodeSaleId": null,
"taxCodeSaleViz": null,
"cost": 0,
"listPrice": 0,
"price": 0,
"taxName": null,
"taxAPct": 0,
"taxBPct": 0,
"taxOnTax": false,
"taxAViz": 0,
"taxBViz": 0,
"lineTotalViz": 0,
"isDirty": false,
"workOrderItemId": 20
}
],
"loans": [],
"parts": [],
"partRequests": [],
"scheduledUsers": [
{
"id": 39,
"concurrency": 7981529,
"userId": 40,
"userViz": "Raul Berge",
"estimatedQuantity": 1,
"startDate": "2021-02-01T17:00:00Z",
"stopDate": "2021-02-01T18:00:00Z",
"serviceRateId": null,
"serviceRateViz": null,
"isDirty": false,
"workOrderItemId": 20
},
{
"id": 40,
"concurrency": 7981529,
"userId": 37,
"userViz": "Caleb Kovacek",
"estimatedQuantity": 2,
"startDate": "2021-02-01T17:00:00Z",
"stopDate": "2021-02-01T18:00:00Z",
"serviceRateId": null,
"serviceRateViz": null,
"isDirty": false,
"workOrderItemId": 20
}
],
"tasks": [],
"travels": [],
"units": [],
"outsideServices": []
},
{
"id": 21,
"concurrency": 7981529,
"notes": "itemnotes - 1 ",
"wiki": null,
"customFields": null,
"tags": [],
"workOrderId": 10,
"techNotes": "technotes - 1",
"workorderItemStatusId": 1,
"workorderItemPriorityId": 1,
"requestDate": "2021-02-01T17:01:00Z",
"warrantyService": false,
"sequence": 2,
"isDirty": false,
"expenses": [
{
"id": 41,
"concurrency": 7981529,
"description": null,
"name": "Licensed Concrete Sausages",
"totalCost": 4.596012142764409,
"chargeAmount": 5.515214571317291,
"taxPaid": 0,
"chargeTaxCodeId": 2,
"chargeTaxCodeViz": "Goods only",
"reimburseUser": true,
"userId": 37,
"userViz": "Caleb Kovacek",
"chargeToCustomer": true,
"isDirty": false,
"workOrderItemId": 21
},
{
"id": 42,
"concurrency": 7981529,
"description": null,
"name": "Fantastic Concrete Fish",
"totalCost": 9.192024285528818,
"chargeAmount": 10.111226714081699,
"taxPaid": 0,
"chargeTaxCodeId": 2,
"chargeTaxCodeViz": "Goods only",
"reimburseUser": true,
"userId": 20,
"userViz": "Reta Braun - HeadOffice",
"chargeToCustomer": true,
"isDirty": false,
"workOrderItemId": 21
}
],
"labors": [
{
"id": 41,
"concurrency": 7981529,
"userId": 34,
"userViz": "Matilda Mraz",
"serviceStartDate": "2021-02-01T17:00:00Z",
"serviceStopDate": "2021-02-01T18:00:00Z",
"serviceRateId": 4,
"serviceRateViz": "Salmon PA",
"serviceDetails": "Voluptatibus eos facilis impedit iusto atque nulla.",
"serviceRateQuantity": 1,
"noChargeQuantity": 0,
"serviceBankId": null,
"taxCodeSaleId": null,
"taxCodeSaleViz": null,
"cost": 0,
"listPrice": 0,
"price": 0,
"taxName": null,
"taxAPct": 0,
"taxBPct": 0,
"taxOnTax": false,
"taxAViz": 0,
"taxBViz": 0,
"lineTotalViz": 0,
"isDirty": false,
"workOrderItemId": 21
},
{
"id": 42,
"concurrency": 7981529,
"userId": 36,
"userViz": "Chasity VonRueden",
"serviceStartDate": "2021-02-01T17:00:00Z",
"serviceStopDate": "2021-02-01T18:00:00Z",
"serviceRateId": 2,
"serviceRateViz": "Silver service rate",
"serviceDetails": "Quibusdam autem ut aut.",
"serviceRateQuantity": 2,
"noChargeQuantity": 0,
"serviceBankId": null,
"taxCodeSaleId": null,
"taxCodeSaleViz": null,
"cost": 0,
"listPrice": 0,
"price": 0,
"taxName": null,
"taxAPct": 0,
"taxBPct": 0,
"taxOnTax": false,
"taxAViz": 0,
"taxBViz": 0,
"lineTotalViz": 0,
"isDirty": false,
"workOrderItemId": 21
}
],
"loans": [],
"parts": [],
"partRequests": [],
"scheduledUsers": [
{
"id": 41,
"concurrency": 7981529,
"userId": 27,
"userViz": "Oran Kreiger",
"estimatedQuantity": 1,
"startDate": "2021-02-01T17:00:00Z",
"stopDate": "2021-02-01T18:00:00Z",
"serviceRateId": null,
"serviceRateViz": null,
"isDirty": false,
"workOrderItemId": 21
},
{
"id": 42,
"concurrency": 7981529,
"userId": 29,
"userViz": "Luisa Mayer",
"estimatedQuantity": 2,
"startDate": "2021-02-01T17:00:00Z",
"stopDate": "2021-02-01T18:00:00Z",
"serviceRateId": null,
"serviceRateViz": null,
"isDirty": false,
"workOrderItemId": 21
}
],
"tasks": [],
"travels": [],
"units": [],
"outsideServices": []
}
],
"states": [
{
"id": 37,
"concurrency": 7981529,
"workOrderId": 10,
"workOrderStatusId": 2,
"created": "2021-02-01T17:05:00Z",
"userId": 34,
"userViz": "Matilda Mraz"
},
{
"id": 38,
"concurrency": 7981529,
"workOrderId": 10,
"workOrderStatusId": 3,
"created": "2021-02-01T18:00:00Z",
"userId": 37,
"userViz": "Caleb Kovacek"
},
{
"id": 39,
"concurrency": 7981529,
"workOrderId": 10,
"workOrderStatusId": 1,
"created": "2021-02-01T18:05:00Z",
"userId": 21,
"userViz": "Johnnie Mueller - CustomerFull"
},
{
"id": 40,
"concurrency": 7981529,
"workOrderId": 10,
"workOrderStatusId": 3,
"created": "2021-02-01T19:00:00Z",
"userId": 41,
"userViz": "Palma Huel"
}
],
"isDirty": false,
"isLockedAtServer": false,
"alertViz": null
}
}
*/
}
</script>