[WIP] Разделил компоненты календаря

This commit is contained in:
megavrilinvv
2023-05-25 12:51:06 +03:00
parent 17bf66c00f
commit 34674bd248
19 changed files with 29 additions and 1 deletions

View File

@@ -0,0 +1,189 @@
<template lang="pug">
.calendar-container.flex
calendar-sidebar(
:url="url",
:schedules-data="schedulesData",
:open-form-create="openFormCreateEvent",
:event-statuses="eventStatuses",
@width="changeWidth",
)
calendar-schedule(
:url="url",
:schedules-data="schedulesData",
:current-date="currentDate",
:time-information="timeInformation",
:events-data="eventsData",
:sidebar-width="sidebarWidth",
:event-statuses="eventStatuses",
@previous-date="switchPreviousDate",
@next-date="switchNextDate",
@selected-layout="changeCalendarLayout",
@selected-event="writeEventData",
@delete-event="openModal",
:open-form-create-event="openFormCreateEvent"
)
calendar-form-add-event(
v-model="isOpenForm",
:selected-event-data="selectedEvent",
:event-statuses="eventStatuses",
:time-information="timeInformation",
:current-date="currentDate",
@clear-event-data="clearSelectedEvent"
)
base-modal(
v-model="showModal",
title="Удаление события"
)
calendar-delete-modal(
:event-statuses="eventStatuses",
:owner-event="selectedEvent",
:close-modal="changeShowModal",
@update-events="fetchEventsData"
)
</template>
<script>
//TODO: Вынести emits в массив
import { fetchWrapper } from "../../shared/fetchWrapper.js";
import * as moment from "moment/moment";
import CalendarSchedule from "./components/CalendarSchedule.vue";
import CalendarSidebar from "./components/CalendarSidebar.vue";
import CalendarFormAddEvent from "./components/CalendarFormAddEvent.vue";
import BaseModal from "@/components/base/BaseModal.vue";
import CalendarDeleteModal from "./components/CalendarDeleteModal.vue";
import { statusesConfig } from "@/pages/calendar/utils/statusesConfig";
export default {
name: "TheCalendar",
components: {
CalendarSchedule,
CalendarSidebar,
CalendarFormAddEvent,
BaseModal,
CalendarDeleteModal,
},
props: {
url: String,
},
data() {
return {
sidebarWidth: "72px",
calendarLayout: "",
currentDate: moment(),
selectedEvent: {},
showModal: false,
timeInformation: {
dayStartTime: "08:00",
dayEndTime: "22:00",
},
eventsData: [],
schedulesData: [],
isOpenForm: false,
//changeFormWasClosed: false,
eventStatuses: statusesConfig,
};
},
methods: {
switchPreviousDate() {
this.currentDate = this.currentDate.clone().subtract(1, "day");
},
switchNextDate() {
this.currentDate = this.currentDate.clone().add(1, "day");
},
changeCalendarLayout(option) {
this.calendarLayout = option;
},
saveEventsData(res) {
this.eventsData = res.results;
},
saveSchedulesData(res) {
if (res.results.length === 0) {
this.schedulesData = [];
return;
}
let filteredEmployees = res.results.filter(
(elem) => elem.status !== "DAY_OFF" && elem.status !== "VACATION"
);
if (filteredEmployees.length === 0) {
this.schedulesData = [];
return;
}
this.schedulesData = filteredEmployees;
},
fetchSchedulesData() {
fetchWrapper
.get(
`accounts/schedules/?date_after=${this.currentDate.format(
"YYYY-MM-DD"
)}&date_before=${this.currentDate.format("YYYY-MM-DD")}`
)
.then((res) => this.saveSchedulesData(res));
},
fetchEventsData() {
fetchWrapper
.get(
`registry/event/?limit=100&start=${this.currentDate.format(
"YYYY-MM-DD"
)}T${
this.timeInformation.dayStartTime
}:00Z&end=${this.currentDate.format("YYYY-MM-DD")}T${
this.timeInformation.dayEndTime
}:00Z`
)
.then((res) => this.saveEventsData(res));
},
changeWidth(value) {
this.sidebarWidth = value;
},
openFormCreateEvent() {
this.isOpenForm = true;
},
closeFormCreateEvent() {
this.isOpenForm = false;
this.fetchEventsData();
},
/*setChangeFormState() {
this.changeFormWasClosed = true;
},
resetChangeFormState() {
this.changeFormWasClosed = false;
},*/
writeEventData(eventData) {
this.selectedEvent = eventData;
this.openFormCreateEvent();
},
clearSelectedEvent() {
this.selectedEvent = {};
this.closeFormCreateEvent();
},
openModal(eventData) {
this.selectedEvent = eventData;
this.showModal = true;
},
changeShowModal() {
this.showModal = false;
},
},
watch: {
showModal: function () {
if (this.showModal === false) {
//this.setChangeFormState();
this.clearSelectedEvent();
}
},
currentDate() {
this.fetchSchedulesData();
this.fetchEventsData();
},
},
mounted() {
this.fetchSchedulesData();
this.fetchEventsData();
},
};
</script>
<style lang="sass" scoped>
.calendar-container
width: calc(100vw - 80px)
</style>

View File

@@ -0,0 +1,59 @@
<template lang="pug">
.flex.flex-col(
:style="backgroundExtendedWidth"
)
.flex.flex-col
.line-wrapper
.line.flex.items-center(
v-for="hour in timeCoil",
:key="hour"
)
.middle-line
</template>
<script>
export default {
name: "CalendarBackground",
props: {
timeCoil: Array,
ownersCount: Number,
},
data() {
return {
columnWidth: 0,
defaultColumnWidth: 470,
pixelsPerHour: 62,
};
},
computed: {
backgroundExtendedWidth() {
if (this.ownersCount > 3) {
return {
width: `${this.defaultColumnWidth * this.ownersCount}px`,
};
}
return {
width: "100%",
};
},
},
};
</script>
<style lang="sass" scoped>
.header
height: 48px
.line
border-bottom: 1px solid var(--border-light-grey-color)
height: 62px
&:first-child
height: 63px
border-top: 1px solid var(--border-light-grey-color)
&:last-child
display: none
.middle-line
border-top: 1px dashed var(--border-light-grey-color)
width: 100%
</style>

View File

@@ -0,0 +1,63 @@
<template lang="pug">
.calendar-clock-column.flex.flex-col.items-end.gap-y-43.pb-5.px-3
span.text-base(
v-for="hour in timeCoil",
:key="hour",
:class="currentHourStyle(hour)"
) {{ hour }}
</template>
<script>
export default {
name: "CalendarClockColumn",
props: {
timeCoil: Array,
currentTime: String,
currentDate: Object,
dayEndTime: Number,
isCurrentDate: Boolean,
},
computed: {
currentHour() {
return this.convertTime(this.currentTime, 0, -6);
},
currentMinute() {
return this.convertTime(this.currentTime, 3, -3);
},
isEndDay() {
return this.dayEndTime === this.currentHour && this.currentMinute > 0;
},
},
methods: {
currentHourStyle(elem) {
if (
this.convertTime(elem, 0, 3) === this.currentHour &&
!this.isEndDay &&
this.isCurrentDate
) {
return {
"current-time": true,
"font-bold": true,
};
}
return {
"current-time": false,
"font-medium": true,
};
},
convertTime(str, startIndex, endIndex) {
return parseInt(str.slice(startIndex, endIndex), 10);
},
},
};
</script>
<style lang="sass" scoped>
.current-time
color: var(--bg-event-red-color)
.calendar-clock-column
width: 80px
height: 100%
color: var(--font-dark-blue-color)
background-color: var(--default-white)
</style>

View File

@@ -0,0 +1,259 @@
<template lang="pug">
.calendar-column-wrapper.flex.flex-col
.header.flex.items-center.justify-between.py-2.px-6.top-0
.flex.items-center
base-avatar.mr-2(:size="32", :color="ownerData.color")
img.h-full.object-cover(
:src="url + ownerData.photo",
alt="Team member",
v-if="ownerData.photo"
)
span(v-if="!ownerData.photo") {{ defaultAvatar }}
span.owner-name.font-medium.text-base.mr-6 {{ ownerName }}
img.icon-wrapper.cursor-pointer(src="@/assets/icons/lock.svg")
column-header-checkbox
.body(@dblclick="clickOnBackground")
//.nonworking-time(:style="nonworkingStartTime")
transition-group(name="card")
calendar-event-card(
v-for="event, index in dayEvents",
:key="event.id + index",
:id="event.id",
:ownerEvent="event",
:event-statuses="eventStatuses",
:style="eventCardPosition(event.start, event.end)",
@selected-event="transmitEventData",
@delete-event="transmitDeleteEvent",
:schedule-body-ref="scheduleBodyRef",
)
//.nonworking-time.absolute(:style="nonworkingEndTime")
</template>
<script>
import ColumnHeaderCheckbox from "./CalendarColumnHeaderCheckbox.vue";
import BaseAvatar from "@/components/base/BaseAvatar";
import CalendarEventCard from "./CalendarEventCard.vue";
//import * as moment from "moment/moment";
import TheNotificationProvider from "@/components/Notifications/TheNotificationProvider";
import { addNotification } from "@/components/Notifications/notificationContext";
export default {
name: "CalendarColumn",
components: {
CalendarEventCard,
BaseAvatar,
ColumnHeaderCheckbox,
TheNotificationProvider,
},
props: {
ownerData: Object,
dayEvents: Array,
dayEndTime: Number,
dayStartTime: Number,
eventStatuses: Array,
//changeFormWasClosed: Boolean,
scheduleBodyRef: Node,
url: String,
openFormCreateEvent: Function,
currentDate: {
type: Object,
default() {
return {};
},
},
},
data() {
return {
pixelsPerHour: 62,
};
},
computed: {
/*workingShift() {
if (!this.ownerData.id) return null;
let start = this.ownerData.start_time;
let end = this.ownerData.end_time;
return {
start: start?.split(":"),
end: end?.split(":"),
fulltime: `${start} - ${end}`,
};
},
nonworkingStartTime() {
if (!this.ownerData.id || !this.workingShift) return {};
let height =
(this.workingShift.start[0] - this.dayStartTime) * this.pixelsPerHour +
this.workingShift.start[1] * this.pixelsPerMinute;
if (height <= 0)
return {
height: 0,
display: "none",
};
return {
height: `${height}px`,
};
},
nonworkingEndTime() {
if (!this.ownerData.id || !this.workingShift) return {};
let time = moment()
.hour(this.dayEndTime)
.minute(0)
.subtract(parseInt(this.workingShift.end[0]), "hours")
.subtract(parseInt(this.workingShift.end[1]), "minutes")
.format("HH:mm")
.split(":");
let position =
time[0] * this.pixelsPerHour + time[1] * this.pixelsPerMinute;
if (position <= 0)
return {
bottom: 0,
display: "none",
};
return {
bottom: 0,
height: `${position}px`,
};
},*/
ownerName() {
if (this.ownerData.id) {
let checkedFirstName =
this.ownerData.first_name !== null
? this.ownerData.first_name[0] + "."
: "";
let checkedPatronymic =
this.ownerData.patronymic !== null
? this.ownerData.patronymic[0] + "."
: "";
return `${this.ownerData.last_name} ${checkedFirstName}${checkedPatronymic}`;
}
return null;
},
defaultAvatar() {
let checkedFirstName =
this.ownerData.first_name !== null
? this.ownerData.first_name[0]
: this.ownerData.last_name[1];
return `${this.ownerData.last_name[0]}${checkedFirstName}`;
},
pixelsPerMinute() {
return this.pixelsPerHour / 60;
},
},
methods: {
eventCardPosition(startTime, endTime) {
let start = startTime
.slice(11, -4)
.split(":")
.map((elem) => parseInt(elem, 10));
let end = endTime.slice(11, -6);
let position =
(start[0] - this.dayStartTime) * this.pixelsPerHour +
start[1] * this.pixelsPerMinute;
if (
parseInt(start[0], 10) < this.dayStartTime ||
parseInt(end, 10) > this.dayEndTime
) {
return {
top: "0px",
visibility: "hidden",
};
}
return {
top: `${position}px`,
};
},
transmitEventData(eventData) {
this.$emit("selected-event", eventData);
},
/*transmitResetChangeForm() {
this.$emit("reset-change-form");
},*/
transmitDeleteEvent(eventData) {
this.$emit("delete-event", eventData);
},
addErrorNotification(title, message) {
addNotification(title, title, message, "error", 5000);
},
/*clickOnBackground(e) {
let res = String((e.offsetY / this.pixelsPerHour).toFixed(2)).split(".");
let hours = parseInt(res[0]) + this.dayStartTime;
let minuts = Math.round(res[1] * 0.6);
if (minuts < 10) minuts = "0" + minuts;
if (hours < 10) hours = "0" + hours;
if (e.target.className !== "body")
this.addErrorNotification(
`Для сотрудника ${this.ownerName} рабочие часы: ${this.workingShift.fulltime}`,
"Вы пытаетесь добавить запись вне рабочего времени"
);
else
this.$emit("selected-event", {
employees: [
{
employee: {
id: this.ownerData.id,
last_name: this.ownerData.last_name,
first_name: this.ownerData.first_name,
patronymic: this.ownerData.patronymic,
color: this.ownerData.color,
photo: this.ownerData.photo,
},
role: "owner",
},
],
start: `${this.currentDate.format(
"YYYY-MM-DD"
)}T${hours}:${minuts}:00`,
end: `${this.currentDate.format("YYYY-MM-DD")}T${hours}:${minuts}:00`,
});
},*/
},
};
</script>
<style lang="sass" scoped>
.calendar-column-wrapper
border-right: 1px solid var(--border-light-grey-color)
&:last-child
border-right: none
.header
position: sticky
z-index: 5
background-color: var(--default-white)
.body
position: relative
z-index: 3
height: 100%
.btn
opacity: 0.5
padding: 7px 13px !important
.icon-wrapper
width: 24px
height: 24px
.owner-name
color: var(--font-dark-blue-color)
.card-enter-from
opacity: 0
pointer-events: none
.card-enter-active
transition: 0.3s ease
.card-leave-to
opacity: 0
pointer-events: none
.card-leave-active
transition: 0.3s ease
.card-move
transition: 0.3s ease
.nonworking-time
width: 100%
background-color: var(--btn-blue-sec-color)
opacity: 0.3
</style>

View File

@@ -0,0 +1,42 @@
<template lang="pug">
div.icon-wrap.flex.items-center.justify-center.cursor-pointer.py-1.px-2(
:class="{disable: !isChecked}",
@click="changeState"
)
.icon-doc-ok.text-xxl
</template>
<script>
export default {
name: "CalendarColumnHeaderCheckbox",
data() {
return {
isChecked: false,
};
},
methods: {
changeState() {
this.isChecked = !this.isChecked;
this.$emit("isChecked", this.isChecked);
},
},
};
</script>
<style lang="sass" scoped>
.disable
opacity: 0.5
.icon-wrap
height: 32px
background-color: var(--btn-blue-color-1)
color: var(--btn-blue-color)
border-radius: 4px
&:hover
background-color: var(--btn-blue-color-2)
&:active
background-color: var(--font-dark-blue-color)
color: var(--default-white)
.icon-doc-ok
width: 24px
height: 24px
</style>

View File

@@ -0,0 +1,78 @@
<template lang="pug">
.flex.flex-col.mb-1.mt-4
span.font-medium.text-base.modal-text.mb-3 Вы действительно хотите удалить это событие?
.card-container
calendar-event-description-card(
disabled,
:event-statuses="eventStatuses",
:owner-event="ownerEvent"
)
.flex.gap-x-3.mt-6.font-semibold
q-btn(
label="Отменить",
no-caps,
outline,
color="primary",
padding="10px 22px",
@click="closeModal"
)
q-btn(
label="Удалить",
no-caps,
outline,
style="color: var(--border-red-color)",
padding="10px 22px",
@click="deleteEvent"
)
</template>
<script>
import { fetchWrapper } from "@/shared/fetchWrapper.js";
import CalendarEventDescriptionCard from "./CalendarEventDescriptionCard.vue";
import TheNotificationProvider from "@/components/Notifications/TheNotificationProvider";
import { addNotification } from "@/components/Notifications/notificationContext";
export default {
name: "CalendarDeleteModal",
components: {
CalendarEventDescriptionCard,
TheNotificationProvider,
},
props: {
ownerEvent: Object,
eventStatuses: {
type: Array,
default() {
return [];
},
},
closeModal: Function,
},
methods: {
async deleteEvent() {
await fetchWrapper.del(`registry/event/${this.ownerEvent.id}/delete/`);
this.$emit("update-events");
this.closeModal();
this.addSuccessNotification();
},
addSuccessNotification() {
addNotification(
new Date().getTime(),
"Событие успешно удалено",
"",
"success",
5000
);
},
},
};
</script>
<style lang="sass" scoped>
.modal-text
color: var(--font-grey-color)
.card-container
width: 374px
border-radius: 4px
border: 1px solid var(--border-light-grey-color-1)
</style>

View File

@@ -0,0 +1,348 @@
<template lang="pug">
.wrapper.cursor-pointer.my-1.mx-1(
:style="themeColors",
:class="cardTheme",
ref="eventCard"
)
.card.flex.px-2(
:class="{'py-6px flex-col': longCard}",
:style="cardHeight",
@click="openDescriptionCard",
@dblclick.stop
)
.header.flex.justify-between.items-center(:class="{'items-start': longCard}")
.header-text
span.inline-block.align-middle.font-bold.text-base.mr-4 {{ eventTime }}
span.inline-block.align-middle.font-medium.text-base {{ eventMember }}
.details-count.flex.justify-center.items-center.text-xxs.font-medium(v-if="someDetailsShown") {{ `+${detailsCount}` }}
.body.flex.text-xxs.font-medium(v-if="longCard")
.col.mr-22px
ul
li.mt-2(v-for="elem in descriptionColumns.leftColumn" :key="elem") {{ elem }}
.col
ul
li.mt-2(v-for="elem in descriptionColumns.rightColumn" :key="elem") {{ elem }}
transition(name="description")
calendar-event-description-card(
v-if="isOpenDescriptionCard",
:owner-event="ownerEvent",
:event-statuses="eventStatuses",
@selected-event="transmitEventData",
@close-description-card="closeDescriptionCard",
@delete-event="transmitDeleteEvent",
:schedule-body-ref="scheduleBodyRef"
)
</template>
<script>
import CalendarEventDescriptionCard from "./CalendarEventDescriptionCard.vue";
export default {
name: "CalendarEventCard",
components: { CalendarEventDescriptionCard },
props: {
//changeFormWasClosed: Boolean,
ownerEvent: Object,
eventStatuses: {
type: Array,
default() {
return [];
},
},
scheduleBodyRef: Node,
},
data() {
return {
pixelsPerHour: 62,
isActive: false,
someDetailsShown: true,
isOpenDescriptionCard: false,
};
},
computed: {
pixelsPerMinute() {
return this.pixelsPerHour / 60;
},
eventTime() {
return `${this.trimTime(this.ownerEvent.start)} - ${this.trimTime(
this.ownerEvent.end
)}`;
},
eventMember() {
let membersArray = this.ownerEvent.members;
if (membersArray.length > 1) {
let primaryMember = membersArray.find(
(elem) => elem.role === "primary"
);
return this.composeFullName(primaryMember.person);
}
return this.composeFullName(membersArray[0].person);
},
calculateCardHeight() {
let startTime = this.trimTime(this.ownerEvent.start)
.split(":")
.map((elem) => parseInt(elem, 10));
let endTime = this.trimTime(this.ownerEvent.end)
.split(":")
.map((elem) => parseInt(elem, 10));
return (
(endTime[0] * 60 + endTime[1] - (startTime[0] * 60 + startTime[1])) *
this.pixelsPerMinute
);
},
cardHeight() {
return {
height: `${this.calculateCardHeight - 8}px`,
};
},
cardTheme() {
return {
"active-theme": this.isActive,
"default-theme": !this.isActive,
"long-card": this.longCard,
};
},
themeColors() {
switch (this.ownerEvent.status) {
case this.eventStatuses[1].value:
return {
"--bg-color": "var(--bg-event-grey-color-0)",
"--bg-active": this.eventStatuses[1].color,
"--bg-hover": "var(--bg-event-grey-color-1)",
"--font-color": "var(--font-black-color)",
"--font-active-color": "var(--default-white)",
"--count-color": this.eventStatuses[1].color,
};
case this.eventStatuses[2].value:
return {
"--bg-color": "var(--bg-event-yellow-color-0)",
"--bg-active": this.eventStatuses[2].color,
"--bg-hover": "var(--bg-event-yellow-color-1)",
"--font-color": "var(--font-black-color)",
"--font-active-color": "var(--font-black-color)",
"--count-color": "var(--font-black-color)",
};
case this.eventStatuses[3].value:
return {
"--bg-color": "var(--bg-event-orange-color-0)",
"--bg-active": this.eventStatuses[3].color,
"--bg-hover": "var(--bg-event-orange-color-1)",
"--font-color": "var(--font-black-color)",
"--font-active-color": "var(--font-black-color)",
"--count-color": "var(--font-black-color)",
};
case this.eventStatuses[4].value:
return {
"--bg-color": "var(--bg-event-blue-color-0)",
"--bg-active": this.eventStatuses[4].color,
"--bg-hover": "var(--bg-event-blue-color-1)",
"--font-color": "var(--font-black-color)",
"--font-active-color": "var(--default-white)",
"--count-color": this.eventStatuses[4].color,
};
case this.eventStatuses[5].value:
return {
"--bg-color": "var(--bg-event-green-color-0)",
"--bg-active": this.eventStatuses[5].color,
"--bg-hover": "var(--bg-event-green-color-1)",
"--font-color": "var(--font-black-color)",
"--font-active-color": "var(--default-white)",
"--count-color": this.eventStatuses[5].color,
};
case this.eventStatuses[6].value:
return {
"--bg-color": "var(--bg-event-red-color-0)",
"--bg-active": this.eventStatuses[6].color,
"--bg-hover": "var(--bg-event-red-color-1)",
"--font-color": "var(--font-black-color)",
"--font-active-color": "var(--default-white)",
"--count-color": this.eventStatuses[6].color,
};
default:
return {
"--bg-color": "var(--default-white)",
"--bg-active": "var(--btn-blue-color)",
"--bg-hover": "var(--bg-event-default-hover-color)",
"--font-color": "var(--font-black-color)",
"--font-active-color": "var(--default-white)",
"--border-color": "#b3c3f3",
"--border-active-color": "var(--btn-blue-color)",
"--count-color": "var(--btn-blue-color)",
};
}
},
description() {
return this.ownerEvent.description
? this.ownerEvent.description.split(", ")
: [];
},
descriptionColumns() {
let leftCol = [],
rightCol = [],
heightParts = parseInt((this.calculateCardHeight - 8) / 23);
if (this.ownerEvent.description && heightParts > 1) {
let n = heightParts;
for (let i = 0; i < n * 2 - 2; i++) {
if (!this.description[i]) break;
i % 2 === 0
? leftCol.push(this.description[i])
: rightCol.push(this.description[i]);
}
}
return {
leftColumn: leftCol,
rightColumn: rightCol,
};
},
longCard() {
return parseInt((this.calculateCardHeight - 8) / 23) > 1 ? true : false;
},
detailsCount() {
let columnsLength =
this.descriptionColumns.leftColumn.length +
this.descriptionColumns.rightColumn.length,
remainingDetails = this.description.length - columnsLength;
if (!remainingDetails) this.changeDetailsShown();
return remainingDetails;
},
},
methods: {
trimTime(time) {
return time.slice(11, 16);
},
composeFullName(object) {
return `${object.last_name} ${object.first_name ?? ""} ${
object.patronymic ?? ""
}`;
},
setActiveTheme() {
this.isActive = true;
},
setDefaultTheme() {
this.isActive = false;
},
changeDetailsShown() {
this.someDetailsShown = false;
},
transmitEventData() {
this.$emit("selected-event", this.ownerEvent);
this.hideDescriptionCard();
},
transmitDeleteEvent() {
this.$emit("delete-event", this.ownerEvent);
},
hideDescriptionCard() {
this.isOpenDescriptionCard = false;
},
openDescriptionCard() {
if (!this.isOpenDescriptionCard && !this.isActive) {
this.isOpenDescriptionCard = true;
this.setActiveTheme();
}
},
closeDescriptionCard() {
this.hideDescriptionCard();
this.setDefaultTheme();
},
},
/*watch: {
changeFormWasClosed: {
immediate: true,
handler(newValue) {
if (newValue === true && this.isActive) {
this.isActive = false;
this.$emit("reset-change-form");
}
},
},
},*/
};
</script>
<style lang="sass" scoped>
.description-enter-from
opacity: 0
transform: translateY(2px)
pointer-events: none
.description-enter-active
transition: 0.5s ease
.description-leave-to
opacity: 0
transform: translateY(2px)
pointer-events: none
.description-leave-active
transition: 0.5s ease
.description-move
transition: 0.5s ease
.wrapper
position: absolute
width: calc(100% - 8px)
.default-theme
z-index: 2
.card
background-color: var(--bg-color)
border: 2px solid var(--border-color)
border-left: 4px solid var(--bg-active)
.card:hover
background-color: var(--bg-hover)
.header-text, .body
color: var(--font-color)
.details-count
background-color: var(--bg-active)
color: var(--font-active-color)
li:before
background-color: var(--font-color)
.active-theme
z-index: 3
.card
background-color: var(--bg-active)
border: 2px solid var(--border-active-color)
border-left: 4px solid var(--bg-active)
.header-text, .body
color: var(--font-active-color)
.details-count
background-color: var(--default-white)
color: var(--count-color)
li:before
background-color: var(--font-active-color)
.card
border-radius: 4px
min-height: 23px
.header
width: 100%
.details-count
width: 24px
height: 16px
border-radius: 16px
.col
max-width: calc(462px/2 - 22px)
ul
list-style-type: none
margin: 0
padding: 0
li
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
li:before
content: ''
display: inline-block
height: 8px
width: 8px
border-radius: 50%
background-color : var(--font-black-color)
margin-right: 4px
</style>

View File

@@ -0,0 +1,214 @@
<template lang="pug">
.wrapper.px-4.pt-14px.pb-4.font-medium.cursor-auto(
:style="{...typeColor, ...position, ...constantWidth}",
v-click-outside="close",
:class="{'shadow': !disabled}",
ref="descriptionCard"
)
.flex.justify-between.items-center.mb-2
.flex
span.inline-block.font-bold.text-base.mr-3.mt-2px {{ eventTime }}
.type.text-xs.font-medium.flex.items-center.justify-center.px-14px(v-if="isCertainType")
span.type-text {{ status }}
.right-side.flex.gap-x-4.text-sm(v-if="!disabled")
.icon-basket.flex.items-center.cursor-pointer(@click="transmitDeleteEvent")
.icon-edit.flex.items-center.cursor-pointer(@click="transmitEventData")
.icon-cancel.text-xxs.flex.items-center.cursor-pointer(@click="close")
.body.mr-6
span.text-base {{ eventMember }}
.flex.text-xxs.justify-between(
v-if="!disabled",
:class="{'mt-4': description.length > 0}"
)
.column
ul
li(v-for="elem in description" :key="elem") {{ elem }}
</template>
<script>
import { statusesConfig } from "@/pages/calendar/utils/statusesConfig";
export default {
name: "CalendarEventDescriptionCard",
props: {
ownerEvent: Object,
eventStatuses: {
type: Array,
default() {
return [];
},
},
disabled: {
type: Boolean,
default: false,
},
scheduleBodyRef: Node,
},
data() {
return {
isCertainType: true,
position: {},
};
},
computed: {
status() {
return this.ownerEvent.status
? statusesConfig.find((e) => e.value === this.ownerEvent.status).label
: "";
},
typeColor() {
switch (this.ownerEvent.status) {
case this.eventStatuses[1].value:
return {
"--bg-color": "var(--bg-event-grey-color-0)",
"--font-color": this.eventStatuses[1].color,
};
case this.eventStatuses[2].value:
return {
"--bg-color": "var(--bg-event-yellow-color-0)",
"--font-color": "var(--font-black-color)",
};
case this.eventStatuses[3].value:
return {
"--bg-color": "var(--bg-event-orange-color-0)",
"--font-color": "var(--font-black-color)",
};
case this.eventStatuses[4].value:
return {
"--bg-color": "var(--bg-event-blue-color-0)",
"--font-color": this.eventStatuses[4].color,
};
case this.eventStatuses[5].value:
return {
"--bg-color": "var(--bg-event-green-color-0)",
"--font-color": this.eventStatuses[5].color,
};
case this.eventStatuses[6].value:
return {
"--bg-color": "var(--bg-event-red-color-0)",
"--font-color": this.eventStatuses[6].color,
};
default:
return {
"--bg-color": "var(--bg-event-default-hover-color)",
"--font-color": "var(--btn-blue-color)",
};
}
},
eventTime() {
return `${this.trimTime(this.ownerEvent.start)} - ${this.trimTime(
this.ownerEvent.end
)}`;
},
eventMember() {
let membersArray = this.ownerEvent.members;
if (membersArray.length > 1) {
let primaryMember = membersArray.find(
(elem) => elem.role === "primary"
);
return this.composeFullName(primaryMember.person);
}
return this.composeFullName(membersArray[0].person);
},
description() {
return this.ownerEvent.description
? this.ownerEvent.description.split(", ")
: [];
},
constantWidth() {
if (!this.disabled) {
return {
width: "426px !important",
};
}
return "";
},
},
methods: {
changeType() {
this.isCertainType = false;
},
transmitEventData() {
this.$emit("selected-event");
},
transmitDeleteEvent() {
this.$emit("delete-event");
},
close() {
if (!this.disabled) this.$emit("close-description-card");
},
trimTime(time) {
return time.slice(11, 16);
},
composeFullName(object) {
return `${object.last_name} ${object.first_name ?? ""} ${
object.patronymic ?? ""
}`;
},
},
mounted() {
if (!this.$refs.descriptionCard || !this.scheduleBodyRef) return;
const cardRect = this.$refs.descriptionCard.getBoundingClientRect();
const bodyRect = this.scheduleBodyRef.getBoundingClientRect();
const bodyHeight = this.scheduleBodyRef.clientHeight + bodyRect.y - 20;
if (cardRect.y + cardRect.height > bodyHeight) {
this.position = {
top: `-${cardRect.height + 8}px`,
};
} else
this.position = {
"margin-top": "8px",
};
},
};
</script>
<style lang="sass" scoped>
.wrapper
height: auto !important
background-color: var(--default-white)
color: var(--font-dark-blue-color)
.shadow
border-radius: 4px
box-shadow: var(--default-shadow)
.type
background-color: var(--bg-color)
border-radius: 4px
.type-text
color: var(--font-color)
.right-side
height: 16px
color: var(--btn-blue-color)
.icon-basket
color: var(--border-red-color)
.column
max-width: calc(462px/2 - 36px)
ul
list-style-type: none
margin: 0
padding: 0
li
margin-bottom: 8px
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
&:last-child
margin: 0px
li:before
content: ''
display: inline-block
height: 6px
width: 6px
border-radius: 50%
background-color: var(--font-black-color)
margin: 0 8px 1px 0
</style>

View File

@@ -0,0 +1,645 @@
<template lang="pug">
base-modal(
v-model="value",
:title="!selectedEventData.id ? 'Запись на прием' : 'Изменение записи'",
draggable,
hide-overlay
)
.event-form.flex.flex-col.gap-y-6.pt-8
.flex.flex-col.gap-y-8
base-select(
v-if="selectedEventData.id"
v-model="status",
:items="statusesList",
label="Статус приема"
)
base-input(
v-else,
disabled,
v-model="status.label",
label="Статус приема",
outlined
)
base-select(
:items="ownersList",
v-model="employees.employee",
placeholder="Выберите сотрудника"
label="Сотрудник"
)
base-select(
:items="membersList",
v-model="members.person",
placeholder="Выберите клиента",
label="Клиент"
)
.flex.gap-x-4
base-input(
v-model="eventDate",
label="Дата",
type="date",
outlined
)
.flex.gap-x-2.items-center.justify-between
base-input(
type="time",
v-model="startTime",
label="Начало",
outlined
)
span.mt-4
base-input(
type="time",
v-model="endTime",
label="Конец",
outlined
)
q-btn.create-button(
v-if="!selectedEventData.id",
label="Создать событие",
no-caps,
color="primary",
padding="10px 22px",
:disabled="disabledCreateButton",
@click="sendEventData",
)
q-btn.update-button(
v-else,
label="Сохранить",
no-caps,
color="primary",
padding="10px 22px",
:disabled="disabledUpdateButton",
@click="updateEventData",
)
</template>
<script>
import TheNotificationProvider from "@/components/Notifications/TheNotificationProvider";
import { addNotification } from "@/components/Notifications/notificationContext";
import { fetchWrapper } from "@/shared/fetchWrapper.js";
import BaseInput from "@/components/base/BaseInput.vue";
import BaseSelect from "@/components/base/BaseSelect.vue";
import * as moment from "moment/moment";
import BaseModal from "@/components/base/BaseModal.vue";
import { v_model } from "@/shared/mixins/v-model";
import { statusesConfig } from "@/pages/calendar/utils/statusesConfig";
export default {
name: "FormChangeEvent",
components: {
BaseModal,
BaseSelect,
BaseInput,
TheNotificationProvider,
},
mixins: [v_model],
//emits: ["clear-selected-event-data", "close-change-form"],
emits: ["clear-event-data"],
props: {
//closeForm: Function,
selectedEventData: {
type: Object,
default() {
return {};
},
},
timeInformation: {
type: Object,
default() {
return {};
},
},
currentDate: {
type: Object,
default() {
return {};
},
},
},
data() {
return {
EMPLOYEE_TYPE: "owner",
MEMBER_TYPE: "primary",
EVENT_KIND: "Прием",
eventData: {},
startTime: "",
endTime: "",
members: {},
employees: {},
eventDate: Date,
status: { label: "Планируется прием", id: null },
id: null,
//ifClearedForm: true,
membersData: [],
ownersData: [],
WORKING_STATUS: "WORKS",
};
},
computed: {
ownersList() {
if (this.ownersData) {
let filteredArray = [];
this.ownersData.forEach((elem) => {
filteredArray.push({
id: elem.id,
label: this.trimOwnerName(
elem.last_name,
elem.first_name,
elem.patronymic
),
});
});
return filteredArray;
}
return [];
},
membersList() {
if (this.membersData) {
let filteredArray = [];
this.membersData.forEach((elem) => {
filteredArray.push({
id: elem.id,
label: this.trimMemberName(
elem.last_name,
elem.first_name,
elem.patronymic
),
});
});
return filteredArray;
}
return [];
},
statusesList() {
let filteredArray = [];
statusesConfig.forEach((elem) => {
filteredArray.push({
id: elem.id,
label: elem.label,
});
});
return filteredArray;
},
disabledCreateButton() {
if (
this.eventDate &&
this.startTime &&
this.endTime &&
this.members.person.label &&
this.employees.employee.label &&
this.status.label
) {
return false;
}
return true;
},
disabledUpdateButton() {
let start = moment(this.selectedEventData.start);
let end = moment(this.selectedEventData.end);
if (
moment(this.eventDate).format("YYYY-MM-DD") ===
start.format("YYYY-MM-DD") &&
this.startTime === start.format("HH:mm") &&
this.endTime === end.format("HH:mm") &&
this.findStatus(this.status.label) === this.selectedEventData.status &&
this.employees.employee.id ===
this.personId(this.selectedEventData.employees, "employee") &&
this.members.person.id ===
this.personId(this.selectedEventData.members, "person")
) {
return true;
}
if (!this.eventDate) return true;
return false;
},
startDate() {
return this.selectedEventData.start
? new Date(this.selectedEventData.start)
: this.currentDate.toDate();
},
endDate() {
return this.selectedEventData.end
? new Date(this.selectedEventData.end)
: this.currentDate.toDate();
},
eventEmployee() {
if (this.selectedEventData.employees) {
let foundEmployee = this.selectedEventData.employees.find(
({ role }) => role === this.EMPLOYEE_TYPE
);
let {
employee: { id, last_name, first_name, patronymic },
...rest
} = foundEmployee;
return {
employee: {
id: id,
label: this.trimOwnerName(last_name, first_name, patronymic),
},
id: rest?.id || null,
role: rest?.role || this.EMPLOYEE_TYPE,
};
}
return {
employee: {
id: null,
label: "",
},
role: this.EMPLOYEE_TYPE,
};
},
eventMember() {
if (this.selectedEventData.members) {
let foundMember = {};
if (this.selectedEventData.members.length > 1) {
foundMember = this.selectedEventData.members.find(
({ role }) => role === this.MEMBER_TYPE
);
} else foundMember = this.selectedEventData.members[0];
let {
person: { id, last_name, first_name, patronymic },
...rest
} = foundMember;
return {
person: {
id: id,
label: this.trimMemberName(last_name, first_name, patronymic),
},
id: rest.id,
role: rest?.role || this.MEMBER_TYPE,
};
}
return {
person: {
id: null,
label: "",
},
role: this.MEMBER_TYPE,
};
},
eventId() {
return this.selectedEventData.id ? this.selectedEventData.id : "";
},
eventStatus() {
return this.selectedEventData.status
? {
label: statusesConfig.find(
(e) => e.value === this.selectedEventData.status
).label,
id: statusesConfig.find(
(e) => e.value === this.selectedEventData.status
).id,
}
: {
label: statusesConfig.find((e) => e.value === "PLANNED").label,
id: 0,
};
},
},
methods: {
findStatus(value) {
return statusesConfig.find((e) => e.label === value)?.value;
},
checkTimeLimits(eventTime, scheduleTime, timeType) {
if (timeType === "start")
return (
parseInt(eventTime.split(":")[0]) <
parseInt(scheduleTime.split(":")[0])
);
if (timeType === "end") {
let firstTime = eventTime.split(":").map((elem) => parseInt(elem));
let secondTime = scheduleTime.split(":").map((elem) => parseInt(elem));
if (firstTime[0] === secondTime[0]) return firstTime[1] > secondTime[1];
else return firstTime[0] > secondTime[0];
}
},
checkTime() {
let counter = 0;
let foundedSchedule = {};
let foundedEmployee = this.ownersData.find(
(elem) => elem.id === this.employees.employee.id
);
if (foundedEmployee)
foundedSchedule = foundedEmployee.schedules.find(
(elem) =>
elem.date === moment(this.eventDate).format("YYYY-MM-DD") &&
elem.status === this.WORKING_STATUS
);
if (!foundedSchedule) {
this.addErrorNotification(
"Некорректная дата события",
`В данный день ${this.employees.employee.label} не работает`
);
counter += 1;
return false;
}
if (
this.checkTimeLimits(
this.startTime,
foundedSchedule.start_time,
"start"
)
) {
this.addErrorNotification(
"Некорректное время начала события",
"Время начала события раньше начала рабочего дня"
);
counter += 1;
}
if (this.checkTimeLimits(this.endTime, foundedSchedule.end_time, "end")) {
this.addErrorNotification(
"Некорректное время окончания события",
"Время окончания события позже окончания рабочего дня"
);
counter += 1;
}
if (
this.checkTimeLimits(this.endTime, foundedSchedule.start_time, "start")
) {
this.addErrorNotification(
"Некорректное время окончания события",
"Время окончания события раньше начала рабочего дня"
);
counter += 1;
}
if (
this.checkTimeLimits(this.startTime, foundedSchedule.end_time, "end")
) {
this.addErrorNotification(
"Некорректное время начала события",
"Время начала события позже окончания рабочего дня"
);
counter += 1;
}
return counter === 0;
},
trimOwnerName(lastName, firstName, patronymic) {
let checkedFirstName = firstName !== null ? firstName[0] + "." : "";
let checkedPatronymic = patronymic !== null ? patronymic[0] + "." : "";
return `${lastName} ${checkedFirstName}${checkedPatronymic}`;
},
trimMemberName(lastName, firstName, patronymic) {
return `${lastName} ${firstName ?? ""} ${patronymic ?? ""}`;
},
async sendEventData() {
if (!this.checkTime()) return;
this.eventData = {
start: this.mergeDate(this.eventDate, this.startTime),
end: this.mergeDate(this.eventDate, this.endTime),
status: this.findStatus(this.status.label),
kind: this.EVENT_KIND,
employees: [
this.findPerson(this.ownersData, this.employees, "employee"),
],
members: [this.findPerson(this.membersData, this.members, "person")],
};
await this.postCreateEvent(this.eventData);
this.eventData = {};
this.value = false;
},
async updateEventData() {
if (!this.checkTime()) return;
if (
Object.keys(
this.findPerson(this.ownersData, this.employees, "employee")
).length > 0 &&
Object.keys(this.findPerson(this.membersData, this.members, "person"))
.length > 0
) {
this.eventData = {
start: this.mergeDate(this.eventDate, this.startTime),
end: this.mergeDate(this.eventDate, this.endTime),
status: this.findStatus(this.status.label),
employees: [
this.findPerson(this.ownersData, this.employees, "employee"),
],
members: [this.findPerson(this.membersData, this.members, "person")],
};
await this.postUpdateEvent(this.id, this.eventData);
this.eventData = {};
this.value = false;
} else
this.addErrorNotification(
"Событие не может быть изменено",
"Клиент был удален"
);
},
//clearForm() {
// if (this.selectedEventData.id) this.$emit("close-change-form");
// this.$emit("clear-selected-event-data");
// this.closeForm();
//},
mergeDate(eventDate, time) {
let parseTime = time.split(":");
return moment(eventDate.setHours(parseTime[0], parseTime[1]), 0).format(
"YYYY-MM-DDTHH:mm:ss"
);
},
findPerson(requestedList, object, field) {
let foundPerson = requestedList.find(({ id }) => id === object[field].id);
if (foundPerson) {
let returnedData = {
[field]: {
id: foundPerson.id,
last_name: foundPerson.last_name,
first_name: foundPerson.first_name,
patronymic: foundPerson.patronymic,
color: foundPerson.color,
},
role: object.role,
};
if (object.id) returnedData.id = object.id;
return returnedData;
}
return {};
},
async postCreateEvent(event) {
const response = await fetchWrapper.post("registry/event/create/", event);
if (response.type && response.type === "validation_error") {
this.addErrorNotification(
response.errors[0].code,
response.errors[0].detail
);
} else {
this.addSuccessNotification("Событие успешно создано");
//this.clearForm();
}
},
async postUpdateEvent(id, event) {
const response = await fetchWrapper.post(
`registry/event/${id}/update/`,
event
);
if (response.type && response.type === "validation_error") {
this.addErrorNotification(
response.errors[0].code,
response.errors[0].detail
);
} else {
this.addSuccessNotification("Изменения успешно сохранены");
//this.clearForm();
}
},
personId(object, field) {
if (object) {
let foundPerson = {};
if (field === "employee") {
foundPerson = object.find(({ role }) => role === this.EMPLOYEE_TYPE);
}
if (field === "person") {
if (object.length > 1) {
foundPerson = object.find(({ role }) => role === this.MEMBER_TYPE);
}
foundPerson = object[0];
}
let {
[field]: { id },
} = foundPerson;
return id;
}
},
addErrorNotification(title, message) {
addNotification(title, title, message, "error", 5000);
},
addSuccessNotification(message) {
addNotification(new Date().getTime(), message, "", "success", 5000);
},
fetchMembersData() {
fetchWrapper
.get("general/person/?limit=200")
.then((res) => this.saveMembersData(res));
},
fetchOwnersData() {
fetchWrapper
.get(
`accounts/schedules/?date_after=${this.currentDate
.clone()
.subtract(1, "month")
.format("YYYY-MM-DD")}&date_before=${this.currentDate
.clone()
.add(1, "month")
.format("YYYY-MM-DD")}`
)
.then((res) => this.saveOwnersData(res));
},
saveOwnersData(res) {
let serializedList = [];
if (res.results.length === 0) {
this.ownersData = [];
return;
}
res.results.forEach((elem) => {
let foundedElem = serializedList.find(
(item) => item.id === elem.employee.id
);
if (!foundedElem) {
serializedList.push({
id: elem.employee.id,
last_name: elem.employee.last_name,
first_name: elem.employee.first_name,
patronymic: elem.employee.patronymic,
color: elem.employee.color,
schedules: [
{
date: elem.date,
end_time: elem.end_time?.slice(0, elem.end_time.length - 3),
start_time: elem.start_time?.slice(
0,
elem.start_time.length - 3
),
status: elem.status,
id: elem.id,
},
],
});
} else {
foundedElem.schedules.push({
date: elem.date,
end_time: elem.end_time?.slice(0, elem.end_time.length - 3),
start_time: elem.start_time?.slice(0, elem.start_time.length - 3),
status: elem.status,
id: elem.id,
});
}
});
this.ownersData = serializedList;
},
saveMembersData(res) {
this.membersData = res.results;
},
},
watch: {
startDate: {
immediate: true,
handler(newDate) {
if (newDate) {
this.eventDate = newDate;
this.startTime = moment(newDate).format("HH:mm");
}
},
},
endDate: {
immediate: true,
handler(newDate) {
if (newDate) {
this.endTime = moment(newDate).format("HH:mm");
}
},
},
selectedEventData: {
immediate: true,
handler(newDate) {
if (newDate) {
this.id = this.eventId;
this.status = this.eventStatus;
this.employees = this.eventEmployee;
this.members = this.eventMember;
}
},
},
value(newValue) {
if (newValue === false) this.$emit("clear-event-data");
},
},
mounted() {
this.fetchOwnersData();
this.fetchMembersData();
},
};
</script>
<style lang="sass" scoped>
.event-form
width: 550px
.form-item
border-radius: 4px
width: 344px
background-color: var(--default-white)
.date-input
width: 174px
border-radius: 4px
background-color: var(--default-white)
.icon
width: 24px
height: 24px
color: var(--font-dark-blue-color)
.close-icon
color: var(--font-dark-blue-color)
&:hover
color: var(--btn-blue-color)
.create-button
width: 183px
.select
border: 1.5px solid var(--border-light-grey-color)
border-radius: 4px
.update-button
width: 132px
</style>

View File

@@ -0,0 +1,124 @@
<template lang="pug">
.calendar-header-wrapper.flex.items-center.justify-between.py-4.pl-4.pr-6
.flex.items-center
q-btn.mr-4(
color="secondary",
round,
size="14px",
dense,
icon="arrow_back_ios_new"
text-color="primary",
@click="previousHandler"
)
q-btn.mr-4(
color="secondary",
icon="arrow_forward_ios",
round,
size="14px",
text-color="primary",
dense,
@click="nextHandler"
)
.text.flex.items-center
span.font-medium.text-base {{ dateString }}
span.today.font-bold.text-xxs(v-if="isCurrentDate") Сегодня
//q-btn(
color="blue-grey-1",
round,
size="14px",
text-color="primary",
@click="previousHandler"
icon="arrow_back_ios_new"
//)
//.text.flex.items-center.mx-4
span.font-medium.text-base {{ dateString }}
span.today.font-bold.text-xxs(v-if="isCurrentDate") Сегодня
//q-btn(
color="blue-grey-1",
icon="arrow_forward_ios",
round,
size="14px",
text-color="primary",
@click="nextHandler"
//)
//.flex.gap-x-4.ml-5.border-2.border-green-600.bg-lime-200.items-center
.w-120.h-10.flex.items-center {{ date }}
base-date-picker(v-model="date")
calendar-layout-switch(@selected="changeSelectedLayout")
</template>
<script>
import CalendarLayoutSwitch from "./CalendarLayoutSwitch.vue";
//import BaseDatePicker from "@/components/base/BaseDatePicker.vue";
export default {
name: "CalendarHeader",
components: { CalendarLayoutSwitch },
props: {
currentDate: Object,
isCurrentDate: Boolean,
},
/*data() {
return {
date: new Date(),
};
},*/
computed: {
dateString() {
let newStr = this.currentDate.format("D MMMM YYYY");
return newStr
.split(" ")
.map((elem, index) => {
if (index === 1) return elem[0].toUpperCase() + elem.slice(1);
return elem;
})
.join(" ");
},
},
methods: {
changeSelectedLayout(option) {
this.$emit("selected-layout", option);
},
previousHandler() {
this.$emit("previous-date");
},
nextHandler() {
this.$emit("next-date");
},
},
};
</script>
<style lang="sass" scoped>
.calendar-header-wrapper
width: 100%
background-color: var(--default-white)
height: 72px
border-radius: 4px
z-index: 10
.left-arrow
padding: 3px 4px 0 4px !important
transform: rotate(90deg)
.right-arrow
padding: 3px 4px 0 4px !important
transform: rotate(270deg)
.text
color: var(--font-dark-blue-color)
.today
opacity: 0.5
.bg-blue-grey-1
background: var(--bg-light-grey) !important
//.text-primary
// color: var(--font-dark-blue-color-0) !important
.q-btn--round
width: 32px !important
height: 32px !important
min-width: 32px !important
min-height: 32px !important
</style>

View File

@@ -0,0 +1,52 @@
<template lang="pug">
.layout-switch-wrapper.flex.h-10.p-1
button#day.flex.items-center.px-3.transition.duration-200.ease-linear(
:class="dayLayoutState",
@click="changeSelectedLayout"
) День
button#week.flex.items-center.px-3.transition.duration-200.ease-linear(
:class="weekLayoutState",
@click="changeSelectedLayout"
) Неделя
</template>
<script>
export default {
name: "CalendarLayoutSwitch",
data() {
return {
selectedLayout: "day",
};
},
computed: {
dayLayoutState() {
return {
active: this.selectedLayout === "day",
};
},
weekLayoutState() {
return {
active: this.selectedLayout === "week",
};
},
},
methods: {
changeSelectedLayout(event) {
this.selectedLayout = event.target.id;
this.$emit("selected", event.target.id);
},
},
};
</script>
<style lang="sass" scoped>
.active
background-color: var(--bg-aqua-blue)
color: var(--default-white)
border-radius: 4px
.layout-switch-wrapper
background-color: var(--bg-light-grey)
color: var(--font-grey-color)
border-radius: 4px
</style>

View File

@@ -0,0 +1,424 @@
<template lang="pug">
.schedule.mx-2.pb-22px(
ref="schedule",
:style="scheduleWidth"
)
calendar-header(
:current-date="currentDate",
:is-current-date="isCurrentDate",
@previous-date="previousDate",
@next-date="nextDate",
@selected-layout="selectedLayout"
)
.schedule-body(
@scroll="changeScrollingState",
ref="scheduleBody"
)
.hiding-container.fixed(v-if="isScrolling")
.column-wrapper.flex.ml-20(:style="columnWrapperWidth")
calendar-column(
v-for="(owner, index) in filteredOwners",
:key="owner.id",
:owner-data="owner",
:url="url",
:day-events="filterEventsByOwner(owner)",
:day-start-time="validateStartTime",
:day-end-time="validateEndTime",
:style="columnSize",
:event-statuses="eventStatuses",
@selected-event="transmitEventData",
@delete-event="transmitDeleteEvent",
:schedule-body-ref="$refs.scheduleBody",
:open-form-create-event="openFormCreateEvent",
:current-date="currentDate"
)
.flex.w-full.relative
.time-coil-wrapper.left-0.-mt-12.pt-9
calendar-clock-column(
:timeCoil="timeCoil",
:current-time="currentTime",
:is-current-date="isCurrentDate",
:day-end-time="validateEndTime"
)
.time-circle-indicator.left-74px(
v-if="isShownIndicator",
:style="circleIndicatorLocation"
)
span.time-line-indicator.block.left-20(
v-if="isShownIndicator",
:style="lineIndicatorLocation"
)
.flex(:class="calendarBackgroundWidth")
calendar-background(
:time-coil="timeCoil",
:owners-count="ownersCount"
)
</template>
<script>
import * as moment from "moment/moment";
import CalendarHeader from "./CalendarHeader.vue";
import CalendarBackground from "./CalendarBackground.vue";
import CalendarClockColumn from "./CalendarClockColumn.vue";
import CalendarColumn from "./CalendarColumn.vue";
export default {
name: "CalendarSchedule",
components: {
CalendarHeader,
CalendarBackground,
CalendarClockColumn,
CalendarColumn,
},
props: {
openFormCreateEvent: Function,
url: String,
//changeFormWasClosed: Boolean,
currentDate: {
type: Object,
default() {
return {};
},
},
timeInformation: {
type: Object,
default() {
return {};
},
},
eventsData: {
type: Array,
default() {
return [];
},
},
sidebarWidth: String,
schedulesData: {
type: Array,
default() {
return [];
},
},
eventStatuses: {
type: Array,
default() {
return [];
},
},
},
data() {
return {
currentTime: "",
timeCoil: [],
timer: null,
isCurrentDate: true,
isShownIndicator: true,
pixelsPerHour: 62,
columnHeaderHeight: 48,
defaultColumnWidth: 470,
isScrolling: false,
};
},
computed: {
hours() {
return this.convertTime(this.currentTime, 0, -6);
},
minutes() {
return this.convertTime(this.currentTime, 3, -3);
},
hoursMinutes() {
return this.currentTime.slice(0, -3);
},
validateStartTime() {
return this.verifyTime(this.timeInformation.dayStartTime);
},
validateEndTime() {
return this.verifyTime(this.timeInformation.dayEndTime);
},
lineIndicatorLocation() {
if (this.ownersCount > 3) {
return {
width: `${this.defaultColumnWidth * this.ownersCount}px`,
top: `${this.calculateIndicatorLocation()}px`,
};
}
return {
width: "calc(100% - 80px)",
top: `${this.calculateIndicatorLocation()}px`,
};
},
circleIndicatorLocation() {
return {
top: `${this.calculateIndicatorLocation() + 42}px`,
};
},
pixelsPerMinute() {
return this.pixelsPerHour / 60;
},
scheduleHeight() {
return (
(this.validateEndTime - this.validateStartTime) * this.pixelsPerHour
);
},
scheduleWidth() {
return {
"--sidebar-width": this.sidebarWidth,
};
},
filteredOwners() {
let filteredArray = [];
this.schedulesData.forEach((elem) => {
filteredArray.push({
id: elem.employee?.id,
last_name: elem.employee?.last_name,
first_name: elem.employee?.first_name,
patronymic: elem.employee?.patronymic,
color: elem.employee?.color,
photo: elem.employee?.photo,
end_time: elem.end_time?.slice(0, elem.end_time.length - 3),
start_time: elem.start_time?.slice(0, elem.start_time.length - 3),
});
});
return filteredArray;
},
ownersCount() {
return this.filteredOwners.length;
},
columnHeight() {
return (
(this.timeCoil.length - 1) * this.pixelsPerHour +
this.columnHeaderHeight
);
},
columnSize() {
if (this.ownersCount > 3) {
return {
height: `${this.columnHeight}px`,
width: `${this.defaultColumnWidth}px`,
};
}
return {
height: `${this.columnHeight}px`,
width: `calc(100% / ${this.ownersCount})`,
};
},
columnWrapperWidth() {
if (this.ownersCount > 3) {
return {
width: `${this.defaultColumnWidth * this.ownersCount}px`,
};
}
return {
width: "calc(100% - 80px)",
};
},
calendarBackgroundWidth() {
return {
"w-full": this.ownersCount <= 3,
};
},
},
methods: {
previousDate() {
this.$emit("previous-date");
},
nextDate() {
this.$emit("next-date");
},
selectedLayout(option) {
this.$emit("selected-layout", option);
},
startTimer() {
if (
this.hours >= this.validateStartTime &&
this.hours < this.validateEndTime
) {
this.timer = setInterval(() => {
this.changeCurrentTime();
this.changeTimeCoil();
}, 5000);
}
},
stopTimer() {
clearInterval(this.timer);
this.timer = null;
},
changeCurrentTime() {
this.currentTime = moment().format("HH:mm:ss");
},
timeCoilInitialization() {
this.timeCoil = [];
for (let i = this.validateStartTime; i <= this.validateEndTime; i++) {
if (
i === this.hours &&
this.hours !== this.validateEndTime &&
this.isCurrentDate
) {
this.timeCoil.push(this.hoursMinutes);
} else this.timeCoil.push(`${i}:00`);
}
},
changeTimeCoil() {
this.timeCoil = this.timeCoil.map((elem) => {
if (this.convertTime(elem, 0, -3) === this.hours) {
return this.hoursMinutes;
}
return elem.slice(0, -3) + ":00";
});
},
verifyTime(dayTime) {
let timeArray = dayTime.split(":").map((elem) => parseInt(elem, 10));
let newTime = timeArray[1] > 30 ? timeArray[0] + 1 : timeArray[0];
return newTime;
},
convertTime(str, startIndex, endIndex) {
return parseInt(str.slice(startIndex, endIndex), 10);
},
calculateIndicatorLocation() {
let newTime = this.currentTime
.split(":")
.map((elem) => parseInt(elem, 10));
let result =
(newTime[0] - this.validateStartTime) * this.pixelsPerHour +
newTime[1] * this.pixelsPerMinute;
if (result > this.scheduleHeight || result < 0) {
this.isShownIndicator = false;
return 0;
}
return result;
},
findObjectInArray(array, object) {
return array.find(
(item) => JSON.stringify(item) === JSON.stringify(object)
);
},
filterEventsByOwner(owner) {
let filteredArray = [];
this.eventsData.forEach((item) => {
let foundEvent = item.employees.find(
(elem) => elem.employee.id === owner.id && elem.role === "owner"
);
if (foundEvent) filteredArray.push(item);
});
return filteredArray;
},
changeScrollingState(e) {
this.isScrolling = e.target.scrollTop !== 0;
},
showCuttentTime() {
this.$nextTick(() =>
this.$refs["scheduleBody"].scrollTo({
top: `${this.lineIndicatorLocation.top.slice(0, -2) - 240}`,
behavior: "smooth",
})
);
},
/*transmitResetChangeForm() {
this.$emit("reset-change-form");
},*/
transmitEventData(eventData) {
this.$emit("selected-event", eventData);
},
transmitDeleteEvent(eventData) {
this.$emit("delete-event", eventData);
},
bodyScroll() {
this.$nextTick(() => {
let presenceScroll =
this.$refs["scheduleBody"].scrollHeight !==
this.$refs["scheduleBody"].clientHeight;
if (presenceScroll) {
this.$refs["scheduleBody"].classList.add("pr-10px");
this.$refs["schedule"].classList.add("pr-10px");
}
});
},
},
watch: {
currentTime() {
if (
this.hours === this.validateEndTime &&
this.minutes > 0 &&
this.timer
) {
this.stopTimer();
this.timeCoilInitialization();
}
},
currentDate: function () {
this.isCurrentDate =
this.currentDate.format("DD.MM.YYYY") === moment().format("DD.MM.YYYY");
this.isShownIndicator = this.isCurrentDate;
if (this.timer) {
this.stopTimer();
this.timeCoilInitialization();
}
if (this.isCurrentDate) {
this.changeCurrentTime();
this.timeCoilInitialization();
this.startTimer();
this.showCuttentTime();
}
},
},
mounted() {
this.changeCurrentTime();
this.timeCoilInitialization();
this.startTimer();
this.showCuttentTime();
this.bodyScroll();
},
beforeUnmount() {
this.stopTimer();
},
};
</script>
<style lang="sass" scoped>
.schedule
border-top-left-radius: 4px
border-top-right-radius: 4px
position: relative
background-color: var(--default-white)
width: calc(100% - (var(--sidebar-width) + 16px))
height: calc(100vh - 56px - 8px)
.time-line-indicator
border-top: 1px solid var(--bg-event-red-color)
position: absolute
z-index: 4
.time-circle-indicator
width: 12px
height: 12px
background-color: var(--bg-event-red-color)
border-radius: 50%
position: absolute
z-index: 5
.column-wrapper
position: relative
height: 48px
background-color: var(--default-white)
.time-coil-wrapper
position: sticky
z-index: 5
background-color: var(--default-white)
.schedule-body
width: 100%
height: calc(100vh - 56px * 2 - 8px - 22px)
overflow-y: auto
overflow-x: auto
&::-webkit-scrollbar-track:horizontal
margin: 0 24px 0 104px
&::-webkit-scrollbar-track:vertical
margin: 48px 0 24px 0
.hiding-container
width: 80px
height: 48px
top: calc(56px * 2 + 8px)
background-color: var(--default-white)
z-index: 6
</style>

View File

@@ -0,0 +1,113 @@
<template lang="pug">
.sidebar.flex.flex-col.bg-white(:class="openSidebar")
.sidebar-wrapper.h-full.my-13px.flex.flex-col.justify-between(:style="sidebarWidth")
.sidebar-content.items-center.flex.flex-col.gap-y-8.px-4.py-19px
q-btn(
color="primary",
round,
icon="add",
size="13px",
@click="openFormCreate",
v-if="!isOpen"
)
q-btn(
no-caps
label="Создать событие",
color="primary",
class="text-weight-medium, full-width",
icon-right="add",
@click="openFormCreate",
v-else
)
calendar-sidebar-event(:is-open="isOpen", :event-statuses="eventStatuses")
calendar-sidebar-teammate(:team-data="teamData", :is-open="isOpen", :url="url")
//.button-wrapper.flex.justify-center.mb-23px
q-btn(
round,
icon="app:icon-long-arrow",
size="13px",
color="secondary",
text-color="primary"
:style="{ transform: `rotate(${turnButton})`}",
@click="changeSize",
)
</template>
<script>
import CalendarSidebarEvent from "./CalendarSidebarEvent.vue";
import CalendarSidebarTeammate from "./CalendarSidebarTeammate.vue";
export default {
name: "CalendarSidebar",
components: {
CalendarSidebarEvent,
CalendarSidebarTeammate,
},
props: {
schedulesData: Array,
openFormCreate: Function,
eventStatuses: Array,
url: String,
},
data() {
return {
widthSidebarOpen: "232px",
widthSidebarClose: "72px",
isOpen: false,
turnButton: "180deg",
};
},
computed: {
openSidebar() {
return {
"open-sidebar": this.isOpen,
};
},
sidebarWidth() {
if (this.isOpen) {
return {
width: this.widthSidebarOpen,
};
}
return {
width: this.widthSidebarClose,
};
},
teamData() {
if (this.schedulesData.length === 0) return [];
return this.schedulesData.map((elem) => elem.employee);
},
},
methods: {
changeSize() {
this.isOpen = !this.isOpen;
this.$emit(
"width",
this.isOpen ? this.widthSidebarOpen : this.widthSidebarClose
);
this.turnButton = this.isOpen ? "0deg" : "180deg";
},
},
};
</script>
<style lang="sass" scoped>
.sidebar
border-top-right-radius: 4px
.sidebar-wrapper
border-left: 2px solid var(--btn-blue-color-3)
.open-sidebar
.sidebar-content
display: flex
flex-direction: column
row-gap: 32px
padding: 19px 16px
align-items: center
width: 232px
.button-wrapper
justify-content: end
padding-right: 16px
</style>

View File

@@ -0,0 +1,82 @@
<template lang="pug">
.flex.flex-col.items-center.w-full
.flex.flex-col.items-center(v-if="!isOpen")
q-btn(
round,
icon="add",
size="12px",
text-color="primary",
color="secondary"
padding="4px",
dense
)
.flex.flex-col.gap-y-2.items-center.mt-4
.event.flex.items-center.justify-center(v-for="event in eventStatuses" :key="event.id")
.event-type(:style="{ background: event.color }")
.flex.flex-col.gap-y-4.w-full(v-else)
.flex.items-center.justify-between
.flex.text-base.font-bold(:style="{ color: 'var(--font-dark-blue-color)' }") Виды событий
q-btn(
v-if="isOpen",
round,
icon="add",
size="12px",
text-color="primary",
color="secondary"
padding="4px"
)
.flex.flex-col.gap-y-2
.relative.flex.items-center.gap-x-3.h-8(v-for="event in eventStatuses")
input.custom-input.py-2.pl-6.h-full.not-italic.font-medium.text-xxs(
:placeholder="event.label"
:key="event.id"
)
.event-open(:style="{ background: event.color }")
span.icon-edit.cursor-pointer
</template>
<script>
export default {
name: "CalendarSidebarEvent",
props: {
isOpen: Boolean,
eventStatuses: Array,
},
};
</script>
<style lang="sass" scoped>
.event
width: 32px
height: 32px
background: var(--btn-blue-sec-color)
border-radius: 4px
.event-type
width: 16px
height: 16px
border-radius: 2px
.custom-input
padding-top: 9px
padding-left: 24px
border: none
border-radius: 4px
width: 169px
outline: none
background-color: var(--btn-blue-sec-color)
&::placeholder
color: var(--font-dark-blue-color)
&:focus
background-color: var(--font-dark-blue-color)
color: var(--default-white)
&::placeholder
color: var(--default-white)
.event-open
position: absolute
width: 8px
height: 16px
top: 8px
left: 8px
</style>

View File

@@ -0,0 +1,86 @@
<template lang="pug">
.flex.flex-col.items-center.gap-y-2.justify-center(v-if="!isOpen")
q-btn(
round,
icon="add",
size="12px",
text-color="primary",
color="secondary"
padding="4px",
dense
)
.team-card(v-for="teammate in teamData" :key="teammate.id")
base-avatar(:size="32", :color="teammate.color")
img.h-full.object-cover(:src="url + teammate.photo", alt="Team member", v-if="teammate.photo")
span(v-if="!teammate.photo") {{teammateAvatar(teammate)}}
.flex.flex-col.gap-y-4.w-full(v-else, :style="{ color: 'var(--font-dark-blue-color)' }")
.flex.items-center.justify-between
.flex.text-base.font-bold Команды
q-btn(
round,
icon="add",
size="12px",
text-color="primary",
color="secondary"
padding="4px",
dense
)
.box-team.flex.flex-col.gap-y-2
.team-card.flex.items-center.justify-between.cursor-pointer(
v-for="teammate in teamData",
:key="teammate.id"
)
.flex.items-center
base-avatar(:size="32", :color="teammate.color")
img.h-full.object-cover(:src="url + teammate.photo", alt="Team member", v-if="teammate.photo")
span(v-if="!teammate.photo") {{teammateAvatar(teammate)}}
.flex.ml-2.not-italic.font-medium.text-xxs {{ changeName(teammate.last_name, teammate.first_name, teammate.patronymic) }}
span.icon-change-place.cursor-pointer.w-5.flex.items-center.justify-center.w-6.h-6
</template>
<script>
import BaseAvatar from "@/components/base/BaseAvatar";
export default {
name: "CalendarSidebarTeammate",
props: {
teamData: Array,
isOpen: Boolean,
url: String,
},
components: {
BaseAvatar,
},
data() {
return {
maxLengthLastName: 13,
};
},
methods: {
changeName(lastName, firstName, patronymic) {
let checkedFirstName = firstName !== null ? firstName[0] + "." : "";
let checkedPatronymic = patronymic !== null ? patronymic[0] + "." : "";
return lastName.length > this.maxLengthLastName
? lastName.slice(0, this.maxLengthLastName) +
"... " +
checkedFirstName +
checkedPatronymic
: lastName + " " + checkedFirstName + checkedPatronymic;
},
teammateAvatar(teammate) {
let checkedFirstName =
teammate.first_name !== null
? teammate.first_name[0]
: teammate.last_name[1];
return `${teammate.last_name[0]}${checkedFirstName}`;
},
},
};
</script>
<style lang="sass" scoped>
.team-card
border-radius: 16px
&:hover
background: var(--font-dark-blue-color)
color: var(--default-white)
</style>

View File

@@ -0,0 +1,44 @@
export const statusesConfig = [
{
id: 0,
label: "Планируется прием",
value: "PLANNED",
color: "var(--default-white)",
},
{
id: 1,
label: "Отменил",
value: "CANCELED",
color: "var(--bg-event-grey-color)",
},
{
id: 2,
label: "Ожидается прием",
value: "EXPECTED",
color: "var(--bg-event-yellow-color)",
},
{
id: 3,
label: "Опаздывает",
value: "LATE",
color: "var(--bg-event-orange-color)",
},
{
id: 4,
label: "Идет прием",
value: "RECEPTION",
color: "var(--bg-event-blue-color)",
},
{
id: 5,
label: "Прием завершен",
value: "COMPLETED",
color: "var(--bg-event-green-color)",
},
{
id: 6,
label: "Не пришел",
value: "DID_NOT_COME",
color: "var(--bg-event-red-color)",
},
];