first commit
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<div ref="container"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Svg, SVG } from '@svgdotjs/svg.js';
|
||||
import throttle from 'lodash/throttle';
|
||||
|
||||
const daysOfWeek = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
|
||||
|
||||
const cellWidth = 100 / daysOfWeek.length;
|
||||
const cellHeight = 32;
|
||||
|
||||
export default {
|
||||
props: {
|
||||
tasksByDay: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
created() {
|
||||
window.addEventListener('resize', this.resize);
|
||||
},
|
||||
mounted() {
|
||||
const { container } = this.$refs;
|
||||
this.svg = SVG().addTo(container);
|
||||
|
||||
this.resize();
|
||||
this.draw();
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('resize', this.resize);
|
||||
},
|
||||
watch: {
|
||||
tasksByDay() {
|
||||
this.resize();
|
||||
this.draw();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
resize: throttle(function () {
|
||||
const { container } = this.$refs;
|
||||
|
||||
const weeks = Math.ceil(this.tasksByDay.length / 7);
|
||||
const rows = 1 + 2 * weeks;
|
||||
|
||||
const width = container.clientWidth;
|
||||
const height = rows * cellHeight;
|
||||
|
||||
this.svg.viewbox(0, 0, width, height);
|
||||
}, 100),
|
||||
draw: throttle(function () {
|
||||
const borderColor = '#eeeef5';
|
||||
const blockColor = '#fff';
|
||||
const textColor = 'rgb(63, 83, 110)';
|
||||
|
||||
/** @type {Svg} */
|
||||
const svg = this.svg;
|
||||
svg.clear();
|
||||
|
||||
const group = svg.group();
|
||||
group.clipWith(svg.rect('100%', '100%').rx(20).ry(20));
|
||||
|
||||
let horizontalOffset = 0;
|
||||
let verticalOffset = 0;
|
||||
|
||||
const drawHeader = () => {
|
||||
for (const day of daysOfWeek) {
|
||||
const rect = group
|
||||
.rect(`${cellWidth.toFixed(2)}%`, cellHeight)
|
||||
.move(`${horizontalOffset.toFixed(2)}%`, verticalOffset)
|
||||
.fill(blockColor)
|
||||
.stroke(borderColor);
|
||||
|
||||
group
|
||||
.text(add => add.tspan(this.$t(`calendar.days.${day}`)))
|
||||
.font({ anchor: 'middle', size: 16 })
|
||||
.amove(`${(horizontalOffset + cellWidth / 2).toFixed(2)}%`, verticalOffset + cellHeight / 2)
|
||||
.fill(textColor)
|
||||
.clipWith(rect.clone());
|
||||
|
||||
horizontalOffset += cellWidth;
|
||||
}
|
||||
|
||||
verticalOffset += cellHeight;
|
||||
};
|
||||
|
||||
const drawDays = () => {
|
||||
horizontalOffset = 0;
|
||||
|
||||
const days = this.tasksByDay;
|
||||
for (let i = 0; i < days.length; i++) {
|
||||
const { date, day, tasks } = days[i];
|
||||
|
||||
const onClick = () => this.$emit('show-tasks-modal', { date, tasks });
|
||||
|
||||
const rect = group
|
||||
.rect(`${cellWidth.toFixed(2)}%`, 2 * cellHeight)
|
||||
.move(`${horizontalOffset.toFixed(2)}%`, verticalOffset)
|
||||
.fill(blockColor)
|
||||
.stroke(borderColor)
|
||||
.on('click', onClick);
|
||||
|
||||
const text = group
|
||||
.text(add => add.tspan(day.toString()))
|
||||
.font({ anchor: 'middle', size: 16 })
|
||||
.amove(`${(horizontalOffset + cellWidth / 2).toFixed(2)}%`, verticalOffset + cellHeight / 2)
|
||||
.fill(textColor)
|
||||
.clipWith(rect.clone())
|
||||
.on('click', onClick);
|
||||
|
||||
if (tasks.length > 0) {
|
||||
rect.attr('cursor', 'pointer');
|
||||
text.attr('cursor', 'pointer');
|
||||
group
|
||||
.circle(10)
|
||||
.attr('cx', `${(horizontalOffset + cellWidth / 2).toFixed(2)}%`)
|
||||
.attr('cy', verticalOffset + cellHeight * 1.5)
|
||||
.fill('rgb(177, 177, 190)')
|
||||
.on('click', onClick)
|
||||
.attr('cursor', 'pointer');
|
||||
}
|
||||
|
||||
if (i % daysOfWeek.length === daysOfWeek.length - 1) {
|
||||
horizontalOffset = 0;
|
||||
verticalOffset += 2 * cellHeight;
|
||||
} else {
|
||||
horizontalOffset += cellWidth;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
drawHeader();
|
||||
drawDays();
|
||||
}, 100),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,404 @@
|
||||
<template>
|
||||
<div class="calendar">
|
||||
<div ref="container" class="calendar__svg"></div>
|
||||
|
||||
<div
|
||||
v-show="hoverPopup.show"
|
||||
:style="{
|
||||
left: `${hoverPopup.x}px`,
|
||||
top: `${hoverPopup.y}px`,
|
||||
}"
|
||||
class="calendar__popup popup"
|
||||
>
|
||||
<template v-if="hoverPopup.task">
|
||||
<p class="popup__row">
|
||||
<span class="popup__key">{{ $t('calendar.task.name') }}</span>
|
||||
<span class="popup__value">{{ hoverPopup.task.task_name }}</span>
|
||||
</p>
|
||||
|
||||
<p class="popup__row">
|
||||
<span class="popup__key">{{ $t('calendar.task.status') }}</span>
|
||||
<span class="popup__value">{{ hoverPopup.task.status.name }}</span>
|
||||
</p>
|
||||
|
||||
<p class="popup__row">
|
||||
<span class="popup__key">{{ $t('calendar.task.priority') }}</span>
|
||||
<span class="popup__value">{{ hoverPopup.task.priority.name }}</span>
|
||||
</p>
|
||||
|
||||
<p class="popup__row">
|
||||
<span class="popup__key">{{ $t('calendar.task.estimate') }}</span>
|
||||
<span class="popup__value">{{ formatDuration(hoverPopup.task.estimate) }}</span>
|
||||
</p>
|
||||
|
||||
<p class="popup__row">
|
||||
<span class="popup__key">{{ $t('calendar.task.total_spent_time') }}</span>
|
||||
<span class="popup__value">{{ formatDuration(hoverPopup.task.total_spent_time) }}</span>
|
||||
</p>
|
||||
|
||||
<p class="popup__row">
|
||||
<span class="popup__key">{{ $t('calendar.task.start_date') }}</span>
|
||||
<span class="popup__value">{{ formatDate(hoverPopup.task.start_date) }}</span>
|
||||
</p>
|
||||
|
||||
<p class="popup__row">
|
||||
<span class="popup__key">{{ $t('calendar.task.due_date') }}</span>
|
||||
<span class="popup__value">{{ formatDate(hoverPopup.task.due_date) }}</span>
|
||||
</p>
|
||||
|
||||
<p class="popup__row">
|
||||
<span class="popup__key">{{ $t('calendar.task.forecast_completion_date') }}</span>
|
||||
<span class="popup__value">{{ formatDate(hoverPopup.task.forecast_completion_date) }}</span>
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Svg, SVG } from '@svgdotjs/svg.js';
|
||||
import { formatDurationString } from '@/utils/time';
|
||||
import throttle from 'lodash/throttle';
|
||||
|
||||
const msInDay = 24 * 60 * 60 * 1000;
|
||||
const daysOfWeek = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
|
||||
const months = [
|
||||
'january',
|
||||
'february',
|
||||
'march',
|
||||
'april',
|
||||
'may',
|
||||
'june',
|
||||
'july',
|
||||
'august',
|
||||
'september',
|
||||
'october',
|
||||
'november',
|
||||
'december',
|
||||
];
|
||||
|
||||
const cellWidth = 100 / daysOfWeek.length;
|
||||
const cellHeight = 32;
|
||||
|
||||
const maxTasks = 5;
|
||||
|
||||
export default {
|
||||
props: {
|
||||
tasksByWeek: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
showAll: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hoverPopup: {
|
||||
show: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
task: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
created() {
|
||||
window.addEventListener('resize', this.resize);
|
||||
},
|
||||
mounted() {
|
||||
const { container } = this.$refs;
|
||||
this.svg = SVG().addTo(container);
|
||||
|
||||
this.resize();
|
||||
this.draw();
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('resize', this.resize);
|
||||
},
|
||||
watch: {
|
||||
tasksByWeek() {
|
||||
this.resize();
|
||||
this.draw();
|
||||
},
|
||||
showAll() {
|
||||
this.resize();
|
||||
this.draw();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
formatDuration(value) {
|
||||
return value !== null ? formatDurationString(value) : '—';
|
||||
},
|
||||
formatDate(value) {
|
||||
return value !== null ? value : '—';
|
||||
},
|
||||
resize: throttle(function () {
|
||||
const { container } = this.$refs;
|
||||
|
||||
const weeks = this.tasksByWeek.length;
|
||||
const tasks = this.tasksByWeek.reduce(
|
||||
(acc, item) => acc + (this.showAll ? item.tasks.length : Math.min(maxTasks, item.tasks.length)),
|
||||
0,
|
||||
);
|
||||
|
||||
const rows = 1 + weeks + tasks;
|
||||
|
||||
const width = container.clientWidth;
|
||||
const height = rows * cellHeight;
|
||||
|
||||
this.svg.viewbox(0, 0, width, height);
|
||||
}, 100),
|
||||
draw: throttle(function () {
|
||||
const backgroundColor = '#fafafa';
|
||||
const borderColor = '#eeeef5';
|
||||
const blockColor = '#fff';
|
||||
const textColor = 'rgb(63, 83, 110)';
|
||||
|
||||
/** @type {Svg} */
|
||||
const svg = this.svg;
|
||||
svg.clear();
|
||||
|
||||
const group = svg.group();
|
||||
group.clipWith(svg.rect('100%', '100%').rx(20).ry(20));
|
||||
|
||||
let horizontalOffset = 0;
|
||||
let verticalOffset = 0;
|
||||
|
||||
const drawHeader = () => {
|
||||
for (const day of daysOfWeek) {
|
||||
const rect = group
|
||||
.rect(`${cellWidth.toFixed(2)}%`, cellHeight)
|
||||
.move(`${horizontalOffset.toFixed(2)}%`, verticalOffset)
|
||||
.fill(blockColor)
|
||||
.stroke(borderColor);
|
||||
|
||||
group
|
||||
.text(add => add.tspan(this.$t(`calendar.days.${day}`)))
|
||||
.font({ anchor: 'middle', size: 16 })
|
||||
.amove(`${(horizontalOffset + cellWidth / 2).toFixed(2)}%`, verticalOffset + cellHeight / 2)
|
||||
.fill(textColor)
|
||||
.clipWith(rect.clone());
|
||||
|
||||
horizontalOffset += cellWidth;
|
||||
}
|
||||
|
||||
verticalOffset += cellHeight;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string[]} days
|
||||
*/
|
||||
const drawDaysRow = days => {
|
||||
horizontalOffset = 0;
|
||||
|
||||
for (let i = 0; i < days.length; i++) {
|
||||
const { month, day } = days[i];
|
||||
const rect = group
|
||||
.rect(`${cellWidth.toFixed(2)}%`, cellHeight)
|
||||
.move(`${horizontalOffset.toFixed(2)}%`, verticalOffset)
|
||||
.fill(i < 5 ? blockColor : backgroundColor)
|
||||
.stroke(borderColor);
|
||||
|
||||
const dayText =
|
||||
day === 1 ? `${this.$t(`calendar.months.${months[month - 1]}`)} ${day}` : day.toString();
|
||||
group
|
||||
.text(add => add.tspan(dayText).dmove(-8, 0))
|
||||
.font({ anchor: 'end', size: 16 })
|
||||
.amove(`${(horizontalOffset + cellWidth).toFixed(2)}%`, verticalOffset + cellHeight / 2)
|
||||
.fill(textColor)
|
||||
.clipWith(rect.clone());
|
||||
|
||||
horizontalOffset += cellWidth;
|
||||
}
|
||||
|
||||
group.line(0, verticalOffset, '100%', verticalOffset).stroke({ width: 1, color: '#C5D9E8' });
|
||||
group
|
||||
.line(0, verticalOffset + cellHeight - 1, '100%', verticalOffset + cellHeight - 1)
|
||||
.stroke({ width: 1, color: '#C5D9E8' });
|
||||
|
||||
verticalOffset += cellHeight;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object} task
|
||||
* @param {number} startWeekDay
|
||||
* @param {number} endWeekDay
|
||||
*/
|
||||
const drawTaskRow = (task, startWeekDay, endWeekDay) => {
|
||||
const width = cellWidth * (endWeekDay - startWeekDay + 1);
|
||||
horizontalOffset = cellWidth * startWeekDay;
|
||||
|
||||
group.rect(`100%`, cellHeight).move(0, verticalOffset).fill(backgroundColor).stroke(borderColor);
|
||||
|
||||
const taskHorizontalPadding = 0.2;
|
||||
const taskVerticalPadding = 3;
|
||||
|
||||
const popupWidth = 420;
|
||||
const popupHeight = 220;
|
||||
const onClick = () => {
|
||||
this.$router.push(`/tasks/view/${task.id}`);
|
||||
};
|
||||
const onMouseOver = event => {
|
||||
const rectBBox = rect.bbox();
|
||||
const popupX =
|
||||
event.clientX < this.$refs.container.clientWidth - popupWidth
|
||||
? event.clientX
|
||||
: event.clientX - popupWidth - 40;
|
||||
|
||||
const popupY =
|
||||
rectBBox.y + this.$refs.container.getBoundingClientRect().y <
|
||||
window.innerHeight - popupHeight
|
||||
? rectBBox.y
|
||||
: rectBBox.y - popupHeight;
|
||||
|
||||
this.hoverPopup = {
|
||||
show: true,
|
||||
x: popupX,
|
||||
y: popupY,
|
||||
task,
|
||||
};
|
||||
};
|
||||
|
||||
const onMouseOut = event => {
|
||||
this.hoverPopup = {
|
||||
...this.hoverPopup,
|
||||
show: false,
|
||||
};
|
||||
};
|
||||
|
||||
const rect = group
|
||||
.rect(`${width - 2 * taskHorizontalPadding}%`, cellHeight - 2 * taskVerticalPadding)
|
||||
.move(`${horizontalOffset + taskHorizontalPadding}%`, verticalOffset + taskVerticalPadding)
|
||||
.fill(blockColor)
|
||||
.stroke(borderColor)
|
||||
.on('mouseover', event => {
|
||||
onMouseOver(event);
|
||||
event.target.style.cursor = 'pointer';
|
||||
})
|
||||
.on('mouseout', onMouseOut)
|
||||
.on('click', onClick);
|
||||
|
||||
let pxOffset = 0;
|
||||
if (new Date(task.due_date).getTime() + msInDay < new Date().getTime()) {
|
||||
pxOffset += 2 * taskVerticalPadding;
|
||||
group
|
||||
.rect(cellHeight - 4 * taskVerticalPadding, cellHeight - 4 * taskVerticalPadding)
|
||||
.move(
|
||||
`${horizontalOffset + taskHorizontalPadding}%`,
|
||||
verticalOffset + 2 * taskVerticalPadding,
|
||||
)
|
||||
.transform({ translateX: pxOffset })
|
||||
.fill('#FF5569')
|
||||
.stroke(borderColor)
|
||||
.rx(4)
|
||||
.ry(4)
|
||||
.on('mouseover', event => {
|
||||
onMouseOver(event);
|
||||
event.target.style.cursor = 'pointer';
|
||||
})
|
||||
.on('mouseout', onMouseOut)
|
||||
.on('click', onClick);
|
||||
|
||||
pxOffset += cellHeight - 4 * taskVerticalPadding;
|
||||
}
|
||||
|
||||
if (task.estimate !== null && Number(task.total_spent_time) > Number(task.estimate)) {
|
||||
pxOffset += 2 * taskVerticalPadding;
|
||||
group
|
||||
.rect(cellHeight - 4 * taskVerticalPadding, cellHeight - 4 * taskVerticalPadding)
|
||||
.move(
|
||||
`${horizontalOffset + taskHorizontalPadding}%`,
|
||||
verticalOffset + 2 * taskVerticalPadding,
|
||||
)
|
||||
.transform({ translateX: pxOffset })
|
||||
.fill('#FFC82C')
|
||||
.stroke(borderColor)
|
||||
.rx(4)
|
||||
.ry(4)
|
||||
.on('mouseover', event => {
|
||||
onMouseOver(event);
|
||||
event.target.style.cursor = 'pointer';
|
||||
})
|
||||
.on('mouseout', onMouseOut)
|
||||
.on('click', onClick);
|
||||
|
||||
pxOffset += cellHeight - 4 * taskVerticalPadding;
|
||||
}
|
||||
|
||||
group
|
||||
.text(add => add.tspan(task.task_name).dmove(8, 0))
|
||||
.font({ anchor: 'start', size: 16 })
|
||||
.amove(`${horizontalOffset + taskHorizontalPadding}%`, verticalOffset + cellHeight / 2)
|
||||
.transform({ translateX: pxOffset })
|
||||
.fill(textColor)
|
||||
.clipWith(rect.clone())
|
||||
.on('mouseover', event => {
|
||||
onMouseOver(event);
|
||||
event.target.style.cursor = 'pointer';
|
||||
})
|
||||
.on('mouseout', onMouseOut)
|
||||
.on('click', onClick);
|
||||
verticalOffset += cellHeight;
|
||||
};
|
||||
|
||||
drawHeader();
|
||||
|
||||
for (const { days, tasks } of this.tasksByWeek) {
|
||||
drawDaysRow(days);
|
||||
|
||||
for (const { task, start_week_day, end_week_day } of this.showAll
|
||||
? tasks
|
||||
: tasks.slice(0, maxTasks)) {
|
||||
drawTaskRow(task, start_week_day, end_week_day);
|
||||
}
|
||||
}
|
||||
}, 100),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.calendar {
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
position: relative;
|
||||
|
||||
&__svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__popup {
|
||||
background: #ffffff;
|
||||
border-radius: 20px;
|
||||
border: 0;
|
||||
box-shadow: 0px 7px 64px rgba(0, 0, 0, 0.07);
|
||||
|
||||
position: absolute;
|
||||
display: block;
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.popup {
|
||||
&__row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div v-if="visible" class="tasks-modal">
|
||||
<div class="tasks-modal__header">
|
||||
<h4 class="tasks-modal__title">{{ $t('calendar.tasks', { date: formattedDate }) }}</h4>
|
||||
<at-button @click="close"><i class="icon icon-x"></i></at-button>
|
||||
</div>
|
||||
|
||||
<ul class="tasks-modal__list">
|
||||
<li v-for="task of tasks" :key="task.id" class="tasks-modal__item">
|
||||
<router-link class="tasks-modal__link" :to="`/tasks/view/${task.id}`">
|
||||
{{ task.task_name }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
|
||||
const MODAL_VISIBLE_CLASS = 'modal-visible';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
date: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
tasks: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
visible(value) {
|
||||
if (value) {
|
||||
document.body.classList.add(MODAL_VISIBLE_CLASS);
|
||||
} else {
|
||||
document.body.classList.remove(MODAL_VISIBLE_CLASS);
|
||||
}
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.body.classList.remove(MODAL_VISIBLE_CLASS);
|
||||
},
|
||||
computed: {
|
||||
visible() {
|
||||
return this.tasks.length > 0;
|
||||
},
|
||||
formattedDate() {
|
||||
return moment(this.date).format('LL');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
this.$emit('close');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tasks-modal {
|
||||
background: #fff;
|
||||
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
overflow: auto;
|
||||
|
||||
padding: 0.75em 24px;
|
||||
|
||||
z-index: 10;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
body.modal-visible {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
43
resources/frontend/core/modules/Calendar/locales/en.json
Normal file
43
resources/frontend/core/modules/Calendar/locales/en.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"navigation": {
|
||||
"calendar": "Calendar"
|
||||
},
|
||||
"calendar": {
|
||||
"days": {
|
||||
"monday": "Mon",
|
||||
"tuesday": "Tue",
|
||||
"wednesday": "Wed",
|
||||
"thursday": "Thu",
|
||||
"friday": "Fri",
|
||||
"saturday": "Sat",
|
||||
"sunday": "Sun"
|
||||
},
|
||||
"months": {
|
||||
"january": "Jan.",
|
||||
"february": "Feb.",
|
||||
"april": "Apr.",
|
||||
"march": "Mar.",
|
||||
"may": "May.",
|
||||
"june": "Jun.",
|
||||
"july": "Jul.",
|
||||
"august": "Aug.",
|
||||
"september": "Sep.",
|
||||
"october": "Oct.",
|
||||
"november": "Nov.",
|
||||
"december": "Dec."
|
||||
},
|
||||
"task": {
|
||||
"name": "Name",
|
||||
"status": "Status",
|
||||
"priority": "Priority",
|
||||
"estimate": "Estimate",
|
||||
"total_spent_time": "Total spent time",
|
||||
"start_date": "Start date",
|
||||
"due_date": "Due date",
|
||||
"forecast_completion_date": "Forecast completion date"
|
||||
},
|
||||
"tasks": "Tasks {date}",
|
||||
"show_all": "Show all",
|
||||
"show_first": "Show first 5 tasks"
|
||||
}
|
||||
}
|
||||
43
resources/frontend/core/modules/Calendar/locales/ru.json
Normal file
43
resources/frontend/core/modules/Calendar/locales/ru.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"navigation": {
|
||||
"calendar": "Календарь"
|
||||
},
|
||||
"calendar": {
|
||||
"days": {
|
||||
"monday": "Пн.",
|
||||
"tuesday": "Вт.",
|
||||
"wednesday": "Ср.",
|
||||
"thursday": "Чт.",
|
||||
"friday": "Пт.",
|
||||
"saturday": "Сб.",
|
||||
"sunday": "Вс."
|
||||
},
|
||||
"months": {
|
||||
"january": "Янв.",
|
||||
"february": "Февр.",
|
||||
"april": "Апр.",
|
||||
"march": "Март",
|
||||
"may": "Май",
|
||||
"june": "Июнь",
|
||||
"july": "Июль",
|
||||
"august": "Авг.",
|
||||
"september": "Сент.",
|
||||
"october": "Окт.",
|
||||
"november": "Нояб.",
|
||||
"december": "Дек."
|
||||
},
|
||||
"task": {
|
||||
"name": "Название",
|
||||
"status": "Статус",
|
||||
"priority": "Приоритет",
|
||||
"estimate": "Оценка времени",
|
||||
"total_spent_time": "Всего потрачено времени",
|
||||
"start_date": "Дата начала",
|
||||
"due_date": "Дата завершения",
|
||||
"forecast_completion_date": "Прогнозируемая дата завершения"
|
||||
},
|
||||
"tasks": "Задачи {date}",
|
||||
"show_all": "Показать всё",
|
||||
"show_first": "Показать первые 5 задач"
|
||||
}
|
||||
}
|
||||
25
resources/frontend/core/modules/Calendar/module.init.js
Normal file
25
resources/frontend/core/modules/Calendar/module.init.js
Normal file
@@ -0,0 +1,25 @@
|
||||
export const ModuleConfig = {
|
||||
routerPrefix: 'calendar',
|
||||
loadOrder: 30,
|
||||
moduleName: 'Calendar',
|
||||
};
|
||||
|
||||
export function init(context) {
|
||||
context.addRoute({
|
||||
path: '/calendar',
|
||||
name: context.getModuleRouteName() + '.index',
|
||||
component: () => import(/* webpackChunkName: "calendar" */ './views/Calendar.vue'),
|
||||
});
|
||||
|
||||
context.addNavbarEntry({
|
||||
label: 'navigation.calendar',
|
||||
to: { path: '/calendar' },
|
||||
});
|
||||
|
||||
context.addLocalizationData({
|
||||
en: require('./locales/en'),
|
||||
ru: require('./locales/ru'),
|
||||
});
|
||||
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import axios from '@/config/app';
|
||||
|
||||
export default class CalendarService {
|
||||
/**
|
||||
* @param {string|Date} startAt
|
||||
* @param {string|Date} endAt
|
||||
* @param {null|number|number[]} projectId
|
||||
*/
|
||||
get(startAt, endAt, projectId = null) {
|
||||
return axios.post('tasks/calendar', { start_at: startAt, end_at: endAt, project_id: projectId });
|
||||
}
|
||||
}
|
||||
165
resources/frontend/core/modules/Calendar/views/Calendar.vue
Normal file
165
resources/frontend/core/modules/Calendar/views/Calendar.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<div class="calendar">
|
||||
<h1 class="page-title">{{ $t('navigation.calendar') }}</h1>
|
||||
|
||||
<div class="controls-row">
|
||||
<DatePicker
|
||||
class="controls-row__item"
|
||||
:day="false"
|
||||
:week="false"
|
||||
:range="false"
|
||||
initialTab="month"
|
||||
@change="onDateChange"
|
||||
/>
|
||||
|
||||
<ProjectSelect class="controls-row__item" @change="onProjectsChange" />
|
||||
|
||||
<at-button class="controls-row__item show-all" @click="onShowAllClick">{{
|
||||
showAll ? $t('calendar.show_first') : $t('calendar.show_all')
|
||||
}}</at-button>
|
||||
</div>
|
||||
|
||||
<div class="at-container">
|
||||
<calendar-view
|
||||
class="svg-container svg-container__desktop"
|
||||
:tasks-by-week="tasksByWeek"
|
||||
:show-all="showAll"
|
||||
/>
|
||||
|
||||
<calendar-mobile-view
|
||||
class="svg-container svg-container__mobile"
|
||||
:tasks-by-day="tasksByDay"
|
||||
@show-tasks-modal="showTasksModal"
|
||||
/>
|
||||
|
||||
<tasks-modal :date="modal.date" :tasks="modal.tasks" @close="hideTasksModal" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import DatePicker from '@/components/Calendar';
|
||||
import ProjectSelect from '@/components/ProjectSelect';
|
||||
import CalendarMobileView from '../components/CalendarMobileView.vue';
|
||||
import CalendarView from '../components/CalendarView.vue';
|
||||
import TasksModal from '../components/TasksModal.vue';
|
||||
import CalendarService from '../services/calendar.service';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
const ISO8601_DATE_FORMAT = 'YYYY-MM-DD';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CalendarMobileView,
|
||||
CalendarView,
|
||||
DatePicker,
|
||||
TasksModal,
|
||||
ProjectSelect,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
start: moment().startOf('month').format(ISO8601_DATE_FORMAT),
|
||||
end: moment().endOf('month').format(ISO8601_DATE_FORMAT),
|
||||
projects: [],
|
||||
|
||||
tasksByDay: [],
|
||||
tasksByWeek: [],
|
||||
|
||||
modal: {
|
||||
date: moment().format(ISO8601_DATE_FORMAT),
|
||||
tasks: [],
|
||||
},
|
||||
|
||||
showAll: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.service = new CalendarService();
|
||||
this.load = debounce(this.load, 300);
|
||||
this.load();
|
||||
},
|
||||
methods: {
|
||||
async load() {
|
||||
const response = await this.service.get(this.start, this.end, this.projects);
|
||||
const { tasks, tasks_by_day, tasks_by_week } = response.data.data;
|
||||
|
||||
this.tasksByDay = tasks_by_day.map(day => ({
|
||||
...day,
|
||||
tasks: day.task_ids.map(task_id => tasks[task_id]),
|
||||
}));
|
||||
|
||||
this.tasksByWeek = tasks_by_week.map(week => ({
|
||||
...week,
|
||||
tasks: week.tasks.map(item => ({
|
||||
...item,
|
||||
task: tasks[item.task_id],
|
||||
})),
|
||||
}));
|
||||
},
|
||||
onDateChange({ start, end }) {
|
||||
this.start = moment(start).startOf('month').format(ISO8601_DATE_FORMAT);
|
||||
this.end = moment(end).endOf('month').format(ISO8601_DATE_FORMAT);
|
||||
this.load();
|
||||
},
|
||||
onProjectsChange(projects) {
|
||||
this.projects = projects;
|
||||
this.load();
|
||||
},
|
||||
showTasksModal({ date, tasks }) {
|
||||
this.modal.date = date;
|
||||
this.modal.tasks = tasks;
|
||||
},
|
||||
hideTasksModal() {
|
||||
this.modal.tasks = [];
|
||||
},
|
||||
onShowAllClick() {
|
||||
this.showAll = !this.showAll;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.show-all {
|
||||
display: none;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.svg-container {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&__mobile {
|
||||
display: flex;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__desktop {
|
||||
display: none;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
&::v-deep svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&::v-deep text {
|
||||
dominant-baseline: central;
|
||||
}
|
||||
}
|
||||
|
||||
.controls-row {
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user