Files
raven-client/ayanova/src/views/home-schedule.vue
2021-09-17 18:59:43 +00:00

852 lines
25 KiB
Vue

<template>
<div v-if="formState.ready" v-resize="onResize" class="my-n8">
<!-- `{{ "focus:" + focus }}` {{ diagInfo() }}
{{ evInfo }}-->
<gz-error :error-box-message="formState.errorBoxMessage"></gz-error>
<v-sheet height="64">
<v-toolbar flat>
<v-btn outlined class="mr-4" color="grey darken-2" @click="setToday">
{{ $ay.t("DateRangeToday") }}
</v-btn>
<v-btn fab text small color="grey darken-2" @click="prev">
<v-icon small>$prev</v-icon>
</v-btn>
<v-btn fab text small color="grey darken-2" @click="next">
<v-icon small>$next</v-icon>
</v-btn>
<v-toolbar-title v-if="$refs.calendar">
{{ $refs.calendar.title }}
</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn
class="mr-3"
fab
text
small
color="grey darken-2"
@click="settingsDialog = true"
>
<v-icon small>$ayiCog</v-icon>
</v-btn>
<v-menu bottom right>
<template v-slot:activator="{ on, attrs }">
<v-btn outlined color="grey darken-2" v-bind="attrs" v-on="on">
<span>{{ typeToLabel() }}</span>
<v-icon right>$sort</v-icon>
</v-btn>
</template>
<v-list>
<v-list-item @click="viewType = 'day'">
<v-list-item-title>{{ $ay.t("ScheduleDay") }}</v-list-item-title>
</v-list-item>
<v-list-item @click="viewType = 'week'">
<v-list-item-title>{{ $ay.t("ScheduleWeek") }}</v-list-item-title>
</v-list-item>
<v-list-item @click="viewType = 'month'">
<v-list-item-title>{{
$ay.t("ScheduleMonth")
}}</v-list-item-title>
</v-list-item>
<v-list-item @click="viewType = '4day'">
<v-list-item-title>{{ $ay.t("Schedule4Day") }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-toolbar>
</v-sheet>
<v-sheet :height="calendarHeight">
<v-calendar
ref="calendar"
v-model="focus"
color="primary"
:events="events"
:event-color="getEventColor"
:type="viewType"
:locale="languageName"
@click:event="showevInfo"
@click:more="viewDay"
@click:date="viewDay"
@change="fetchEvents"
@mousedown:event="startDrag"
@mousedown:time="startTime"
@mousemove:time="mouseMove"
@mouseup:time="endDrag"
@mouseleave.native="cancelDrag"
:event-more-text="$ay.t('More')"
>
<template v-slot:event="{ event, timed, eventSummary }">
<div class="v-event-draggable">
<v-icon small :color="event.textColor" class="mr-1">{{
iconForEvent(event.type)
}}</v-icon>
<span :class="event.textColor + '--text'" v-html="eventSummary()" />
</div>
<div
v-if="timed"
class="v-event-drag-bottom"
@mousedown.stop="extendBottom(event)"
></div>
</template>
</v-calendar>
<v-menu
v-model="selectedOpen"
:close-on-content-click="false"
:activator="selectedElement"
offset-x
>
<v-card color="grey lighten-4" min-width="360px" flat>
<v-toolbar>
<v-btn icon @click="openScheduledItem()">
<v-icon>{{ iconForSelectedEvent() }}</v-icon>
</v-btn>
<v-toolbar-title>{{ selectedEvent.name }}</v-toolbar-title>
</v-toolbar>
<v-card-text>
<!--woitemscheduleduser -->
<template v-if="selectedEvent.type == 41">
<div>
<span class="text-h6">{{ $ay.t("WorkOrder") }}:</span>
<span class="text-body-1 ml-2"
>{{ evInfo.serial }}&nbsp; {{ evInfo.customerViz }}</span
>
</div>
<div>
<span class="text-h6">{{ $ay.t("Tags") }}:</span>
<span class="text-body-1 ml-2">{{
$ay.util().formatTags(evInfo.wotags)
}}</span>
</div>
<div>
<span class="text-h6">{{ $ay.t("DashboardScheduled") }}:</span>
<span class="text-body-1 ml-2"
>{{ $ay.dt(evInfo.startDate) }}&nbsp;&mdash;&nbsp;{{
$ay.dt(evInfo.stopDate)
}}</span
>
</div>
<div>
<span class="text-h6"
>{{
$ay.t("WorkOrderItemScheduledUserEstimatedQuantity")
}}:</span
>
<span class="text-body-1 ml-2">{{ evInfo.qty }}</span>
</div>
<div>
<span class="text-h6"
>{{ $ay.t("WorkOrderItemScheduledUserServiceRateID") }}:</span
>
<span class="text-body-1 ml-2">{{ evInfo.rate }}</span>
</div>
<div v-if="evInfo.haswostatus">
<span class="text-h6">{{ $ay.t("WorkOrderStatus") }}:</span>
<span class="text-body-1 ml-2">{{ evInfo.wostatus }}</span>
<v-icon :color="evInfo.wostatuscolor" class="ml-4"
>$ayiFlag</v-icon
>
<v-icon
color="primary"
v-if="evInfo.wostatuslocked"
class="ml-4"
>$ayiLock</v-icon
>
<v-icon
color="primary"
v-if="evInfo.wostatuscompleted"
class="ml-4"
>$ayiCheckCircle</v-icon
>
</div>
<div>
<span class="text-h6"
>{{ $ay.t("WorkOrderItemSummary") }}:</span
>
<span class="text-body-1 ml-2">
<v-icon class="mr-3" :color="evInfo.woitemstatuscolor"
>$ayiCircle</v-icon
>{{ evInfo.woitemstatus }}</span
>
</div>
<div>
<span class="text-h6"
>{{ $ay.t("WorkOrderItemPriorityID") }}:</span
>
<span class="text-body-1 ml-2">
<v-icon class="mr-3" :color="evInfo.woitemprioritycolor"
>$ayiFireAlt</v-icon
>{{ evInfo.woitempriority }}</span
>
</div>
<div>
<span class="text-h6">{{ $ay.t("WorkOrderItemTags") }}:</span>
<span class="text-body-1 ml-2">{{
$ay.util().formatTags(evInfo.woitemtags)
}}</span>
</div>
</template>
</v-card-text>
<v-card-actions>
<v-btn color="primary" text @click="openScheduledItem()">{{
$ay.t("Open")
}}</v-btn>
<v-spacer></v-spacer>
<v-btn color="primary" text @click="selectedOpen = false">
{{ $ay.t("Close") }}
</v-btn>
</v-card-actions>
</v-card>
</v-menu>
</v-sheet>
<template>
<v-row justify="center">
<v-dialog max-width="600px" v-model="settingsDialog">
<v-card>
<v-card-title> </v-card-title>
<v-card-text>
settings here
</v-card-text>
<v-card-actions>
<v-btn text @click="settingsDialog = false" color="primary">{{
$ay.t("Cancel")
}}</v-btn>
<v-spacer></v-spacer>
<v-btn
color="primary"
text
@click="acceptSettings()"
class="ml-4"
>{{ $ay.t("OK") }}</v-btn
>
</v-card-actions>
</v-card>
</v-dialog>
</v-row>
</template>
</div>
</template>
<script>
/*
PLAN: TODO:
What shows in schedule:
Icon, start time abbreviated for month view, for other views it's start-stop abbreviated (I guess wider is assumed), name in single line most important to the left least to the right
WO appt. color is user selected from wopriority/woitempriority/woitemstatus color, wo icon in left corner is NOT colored, color on color sucks, let the person select it to see the more info display
Name=[wonumber customername]
Reviews are review icon, appt color - none
Reminders are reminder icon, appt. color is reminder selected color
More info
unlike v7, v8 doesn't attempt to show a lot of info in the calendar appt. display, instead they click on it to bring up a more info dialog with lots of useful info and a link to open the source record
Drag / drop / extend
will try to support this but if it's not doable then possibly for convenience do it in the More info dialog
SETTINGS:
Save / load from server (or locally only? as formSettings.saved)??
saved with device or saved with account is the heart of it
wo display section:
DISPLAY
Color: wostatus/woitemstatus/woitempriority/none
Weekdays: weekdays to display, copy from pm exclude days thingy
More info: checkbox beside each item below that can be shown in wo
TODO NEXT:
Drag/drop/extend/ create new (wo/reminder) with speed dial fab control in click location using example
https://vuetifyjs.com/en/components/calendars/#drag-and-drop
https://vuetifyjs.com/en/components/floating-action-buttons/#speed-dial
Test: overlapping sched items that start or end outside of view
this is a test of the query in schedulecontroller at server
reporting - make it happen
could just default to regular reporting list, doesn't absolutely need to be a calendar at least at first
as long as it has all appointments consolidated in single list
FUTURE NOT INITIAL RELEASE
(these might be required for service-schedule view for moving around/planning etc visually which is needed there, not necessarily here)
Drag and drop and extend
controls to change time/date etc
*/
// const FORM_CUSTOM_TEMPLATE_KEY = "home-schedule";
const FORM_KEY = "home-schedule";
export default {
async created() {
let vm = this;
try {
await initForm(vm);
window.$gz.eventBus.$on("menu-click", clickHandler);
//----------------------------
generateMenu(vm);
} catch (error) {
window.$gz.errorHandler.handleFormError(error, vm);
} finally {
// //console.log("Setting ready, view Type is:", vm.viewType);
vm.formState.ready = true;
}
},
beforeDestroy() {
saveFormSettings(this);
window.$gz.eventBus.$off("menu-click", clickHandler);
},
data() {
return {
//formCustomTemplateKey: FORM_CUSTOM_TEMPLATE_KEY,
focus: "",
viewType: "month",
firstTime: "8:00",
selectedEvent: {},
selectedElement: null,
selectedOpen: false,
events: [],
evInfo: {},
dragEvent: null,
dragStart: null,
createEvent: null,
createStart: null,
extendOriginal: null,
dragged: false,
dragTimeout: null,
formState: {
ready: false,
dirty: false,
valid: true,
readOnly: false,
loading: true,
errorBoxMessage: null,
appError: null,
serverError: {}
},
rights: window.$gz.role.defaultRightsObject(),
calendarHeight: 600,
settingsDialog: false,
timeZoneName: window.$gz.locale.getResolvedTimeZoneName(),
languageName: window.$gz.locale.getResolvedLanguage(),
hour12: window.$gz.locale.getHour12()
};
},
methods: {
startDrag({ event, timed }) {
console.log("startDrag");
if (event && timed) {
//My work around to disambiguate dragging and clicking
clearTimeout(this.dragTimeout);
this.dragged = false;
this.dragTimeout = setTimeout(() => {
this.dragged = true;
}, 100); // Minimal delay to be regarded as drag instead of click
this.dragEvent = event;
this.dragTime = null;
this.extendOriginal = null;
}
},
startTime(tms) {
//console.log("startTime", tms);
const mouse = this.toTime(tms);
if (this.dragEvent && this.dragTime === null) {
const start = new Date(this.dragEvent.start).getTime();
this.dragTime = mouse - start;
//console.log("startTime dragTime is", this.dragTime);
} else {
this.createStart = this.roundTime(mouse);
console.log(
"startTime::STUB Create new element, popup FAB speeddial here"
);
// //console.log("startTime, create start:", this.createStart);
// this.createEvent = {
// name: `Event #${this.events.length}`,
// color: this.rndElement(this.colors),
// start: this.createStart,
// end: this.createStart,
// timed: true
// };
// //console.log("startTime, pushing event:", this.createEvent);
// this.events.push(this.createEvent);
}
},
extendBottom(event) {
//console.log("extendBottom", event);
this.createEvent = event;
this.createStart = event.start;
this.extendOriginal = event.end;
},
mouseMove(tms) {
// //console.log("moustMove", tms);
const mouse = this.toTime(tms);
//console.log("mouseMove got time:", mouse);
if (this.dragEvent && this.dragTime !== null) {
// console.log("mosueMove:A", {
// dragEvent: this.dragEvent,
// dragTime: this.dragTime
// });
const start = this.dragEvent.start;
const end = this.dragEvent.end;
const duration = end - start;
const newStartTime = mouse - this.dragTime;
const newStart = this.roundTime(newStartTime);
//console.log("mouseMove newStart...", newStart);
const newEnd = newStart + duration;
this.dragEvent.start = newStart;
this.dragEvent.end = newEnd;
} else if (this.createEvent && this.createStart !== null) {
//console.log("mosueMove:B");
const mouseRounded = this.roundTime(mouse, false);
//console.log("mouseMove mouseRounded:", mouseRounded);
const min = Math.min(mouseRounded, this.createStart);
const max = Math.max(mouseRounded, this.createStart);
this.createEvent.start = min;
this.createEvent.end = max;
}
},
endDrag() {
// console.log("endDrag");
this.dragTime = null;
this.dragEvent = null;
this.createEvent = null;
this.createStart = null;
this.extendOriginal = null;
},
cancelDrag() {
console.log("cancelDrag");
if (this.createEvent) {
if (this.extendOriginal) {
this.createEvent.end = this.extendOriginal;
} else {
const i = this.events.indexOf(this.createEvent);
if (i !== -1) {
this.events.splice(i, 1);
}
}
}
this.createEvent = null;
this.createStart = null;
this.dragTime = null;
this.dragEvent = null;
},
roundTime(time, down = true) {
//console.log("roundtime...");
const roundTo = 15; // minutes
const roundDownTime = roundTo * 60 * 1000;
return down
? time - (time % roundDownTime)
: time + (roundDownTime - (time % roundDownTime));
},
toTime(tms) {
//console.log("toTime tms=", tms);
return new Date(
tms.year,
tms.month - 1,
tms.day,
tms.hour,
tms.minute
).getTime();
},
diagInfo() {
if (this.$refs.calendar) {
return (
"cal" +
JSON.stringify({
start: this.$refs.calendar.start,
end: this.$refs.calendar.end,
focus: this.focus,
firstInterval: this.$refs.calendar.firstInterval
})
);
} else {
return "no calendar";
}
},
typeToLabel() {
switch (this.viewType) {
case "month":
return this.$ay.t("ScheduleMonth");
case "week":
return this.$ay.t("ScheduleWeek");
case "day":
return this.$ay.t("ScheduleDay");
case "4day":
return this.$ay.t("Schedule4Day");
}
},
onResize() {
this.calendarHeight = window.innerHeight * 0.84;
},
viewDay({ date }) {
this.focus = date;
this.viewType = "day";
},
getEventColor(event) {
return event.color;
},
setToday() {
this.focus = "";
},
prev() {
this.$refs.calendar.prev();
},
next() {
this.$refs.calendar.next();
},
iconForSelectedEvent() {
return window.$gz.util.iconForType(this.selectedEvent.type);
},
iconForEvent(type) {
return window.$gz.util.iconForType(type);
},
openScheduledItem() {
window.$gz.eventBus.$emit("openobject", {
type: this.selectedEvent.type,
id: this.selectedEvent.id
});
},
async showevInfo({ nativeEvent, event }) {
console.log("showevInfo, dragged: ", this.dragged);
//workaround to disambiguate drag click from view more info click
if (this.dragged) {
return;
//e.preventDefault();
}
//this.dragged = false;
//console.log("showevInfo, dragevent is:", this.dragEvent);
////console.log("showevInfo:event ", JSON.stringify(event));
let route = null;
this.evInfo = {};
switch (event.type) {
case window.$gz.type.WorkOrderItemScheduledUser:
route = `workorder/items/scheduled-users/sched-info/${event.id}`;
break;
}
if (route) {
let res = await window.$gz.api.get(route);
if (!res.error) {
this.evInfo = res.data;
}
}
const open = () => {
this.selectedEvent = event;
this.selectedElement = nativeEvent.target;
requestAnimationFrame(() =>
requestAnimationFrame(() => (this.selectedOpen = true))
);
};
if (this.selectedOpen) {
this.selectedOpen = false;
requestAnimationFrame(() => requestAnimationFrame(() => open()));
} else {
open();
}
nativeEvent.stopPropagation();
},
async fetchEvents({ start, end }) {
// console.trace(
// "UPDATE RANGE:",
// JSON.stringify({ start: start.date, end: end.date })
// );
// //console.log(
// "TZ Offset",
// window.$gz.locale.getTZOffset(this.timeZoneName)
// );
/*
public enum PersonalScheduleWorkOrderColorSource : int
{
None = 0,
WorkOrderStatus = 2,
WorkOrderItemStatus = 3,
WorkOrderItemPriority = 4
}
*/
// console.trace("fetchEvents, viewtype is", this.viewType);
try {
window.$gz.form.deleteAllErrorBoxErrors(this);
let res = await window.$gz.api.post("schedule/personal", {
view: window.$gz.util.calendarViewToAyaNovaEnum(this.viewType),
start: window.$gz.locale.localTimeDateStringToUTC8601String(
`${start.date}T00:00:00`,
this.timeZoneName
),
end: window.$gz.locale.localTimeDateStringToUTC8601String(
`${end.date}T23:59:59`,
this.timeZoneName
),
colorSource: 4,
workOrders: true,
reviews: true,
reminders: true
});
if (res.error) {
this.formState.serverError = res.error;
window.$gz.form.setErrorBoxErrors(this);
} else {
this.events = res.data.map(x => {
return {
...x,
start: window.$gz.locale.utcDateToScheduleCompatibleFormatLocalized(
x.start,
this.timeZoneName,
this.languageName,
this.hour12
),
end: window.$gz.locale.utcDateToScheduleCompatibleFormatLocalized(
x.end,
this.timeZoneName,
this.languageName,
this.hour12
)
};
});
}
} catch (error) {
window.$gz.errorHandler.handleFormError(error, this);
}
}
//eom
}
};
/////////////////////////////
//
//
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 "report":
// let res = await m.vm.$refs.reportSelector.open(
// {
// AType: window.$gz.type.Project,
// selectedRowIds: [m.vm.obj.id]
// },
// m.id
// );
// if (res == null) {
// return;
// }
// window.$gz.form.setLastReport(FORM_KEY, res);
// generateMenu(m.vm);
// break;
case "WorkOrderItemScheduledUserList":
m.vm.$router.push({
name: "svc-workorder-item-scheduled-users",
params: {
aType: window.$gz.type.User,
objectId: m.vm.$store.state.userId,
name: m.vm.$store.state.userName
}
});
break;
case "WorkOrderItemLaborList":
m.vm.$router.push({
name: "svc-workorder-item-labors",
params: {
aType: window.$gz.type.User,
objectId: m.vm.$store.state.userId,
name: m.vm.$store.state.userName
}
});
break;
default:
window.$gz.eventBus.$emit(
"notify-warning",
FORM_KEY + "::context click: [" + m.key + "]"
);
}
}
}
//////////////////////
//
//
function generateMenu(vm) {
let menuOptions = {
isMain: true,
readOnly: vm.formState.readOnly,
icon: "$ayiCalendarDay",
title: "Schedule",
helpUrl: "home-schedule",
menuItems: []
};
// //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,
// notrans: true,
// 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.$store.getters.isScheduleableUser) {
menuOptions.menuItems.push({ divider: true, inset: false });
menuOptions.menuItems.push({
title: "WorkOrderItemScheduledUserList",
icon: "$ayiUserClock",
key: FORM_KEY + ":WorkOrderItemScheduledUserList",
vm: vm
});
menuOptions.menuItems.push({
title: "WorkOrderItemLaborList",
icon: "$ayiHammer",
key: FORM_KEY + ":WorkOrderItemLaborList",
vm: vm
});
//--- /show all ---
}
menuOptions.menuItems.push({ divider: true, inset: false });
window.$gz.eventBus.$emit("menu-change", menuOptions);
}
/////////////////////////////////
//
//
async function initForm(vm) {
await fetchTranslatedText(vm);
getFormSettings(vm);
}
function getFormSettings(vm) {
let formSettings = window.$gz.form.getFormSettings(FORM_KEY);
if (!formSettings || !formSettings.temp || !formSettings.temp.viewType) {
//defaults
formSettings = { temp: { viewType: "month", focus: null } };
}
vm.viewType = formSettings.temp.viewType;
vm.focus = formSettings.temp.focus;
// //console.log("getFormSettings, got:", JSON.stringify(formSettings));
// //console.log("getFormSetting, vm.viewType is:", vm.viewType);
return formSettings;
// formSettings.temp.page = 0;
// formSettings.saved.dataTable.listViewId = -1;
// formSettings.saved.dataTable.unsavedListView = LIST_VIEW.listView;
}
function saveFormSettings(vm) {
let formSettings = window.$gz.form.getFormSettings(FORM_KEY);
formSettings.temp = { viewType: vm.viewType, focus: vm.focus };
window.$gz.form.setFormSettings(FORM_KEY, formSettings);
////console.log("SaveFormSettings, saving:", JSON.stringify(formSettings));
}
//////////////////////////////////////////////////////////
//
// Ensures UI translated text is available
//
async function fetchTranslatedText(vm) {
await window.$gz.translation.cacheTranslations([
"DateRangeToday",
"ScheduleMonth",
"ScheduleDay",
"ScheduleWeek",
"Schedule4Day",
"WorkOrder",
"DashboardScheduled",
"WorkOrderItemPriorityID",
"WorkOrderItemSummary",
"WorkOrderStatus",
"WorkOrderItemScheduledUserEstimatedQuantity",
"WorkOrderItemScheduledUserServiceRateID",
"WorkOrderItemTags"
]);
}
</script>
<style scoped lang="scss">
.v-event-draggable {
padding-left: 6px;
}
.v-event-timed {
user-select: none;
-webkit-user-select: none;
}
.v-event-drag-bottom {
position: absolute;
left: 0;
right: 0;
bottom: 4px;
height: 4px;
cursor: ns-resize;
&::after {
display: none;
position: absolute;
left: 50%;
height: 4px;
border-top: 1px solid white;
border-bottom: 1px solid white;
width: 16px;
margin-left: -8px;
opacity: 0.8;
content: "";
}
&:hover::after {
display: block;
}
}
</style>