376 lines
12 KiB
Vue
376 lines
12 KiB
Vue
<template lang="pug">
|
|
|
|
AddEmployeeModal(
|
|
|
|
:searchData="rows"
|
|
:searchFields="['first_name', 'last_name']"
|
|
:searchRow="addEmployeeRow"
|
|
:modalId="modalId"
|
|
@emitResult="addEmployee($event)"
|
|
)
|
|
|
|
<!-- Schedule Table -->
|
|
.pt-5(v-for="(month, mIndex) in scheduleData" :key="mIndex")
|
|
table(class='table table-bordered table-sm' v-for="(row, rIndex) in month" :key="rIndex")
|
|
thead
|
|
tr
|
|
td.fw-bold(style="width: 150px") {{ format(startDates[mIndex], "MMMM", {locale: de}) }}
|
|
td(v-for="(day, dIndex) in weekdays" :key="dIndex" :style="dIndex === 6 ? 'border-right : 2px solid' : ''" :class="{'text-body text-opacity-50 bg-secondary bg-opacity-10' : row.dates[dIndex] === null}") {{ day }}
|
|
tr
|
|
td.fw-bold() {{ format(startDates[mIndex], "y") }}
|
|
td(v-for="(date, dIndex) in row.dates" :key="dIndex" :style="dIndex === 6 ? 'border-right : 2px solid' : ''" :class="{'bg-secondary bg-opacity-10' : row.dates[dIndex] === null}")
|
|
template(v-if="date !== null") {{ format(date, "dd.MM.")}}
|
|
tbody
|
|
tr(v-for="(employee, eIndex) in row.employees" :key="eIndex")
|
|
td()
|
|
.row.justify-content-between.align-items-center.employee-wrapper
|
|
.col.text-start.ps-3 {{employee.shorthand}}
|
|
.col.text-end
|
|
button.btn.btn-sm(
|
|
@click="removeEmployee(mIndex, rIndex, employee)"
|
|
)
|
|
i.bi-x-lg
|
|
td(
|
|
v-for="(date, dIndex) in row.dates"
|
|
:key="dIndex"
|
|
:style="dIndex === 6 ? 'border-right : 2px solid' : ''"
|
|
:class="{'bg-secondary bg-opacity-10' : row.dates[dIndex] === null, 'selected' : isSelected(mIndex, rIndex, eIndex, dIndex)}"
|
|
tabindex="0"
|
|
@click.exact="row.dates[dIndex] !== null ? select(mIndex, rIndex, eIndex, dIndex) : null"
|
|
@click.ctrl.exact="row.dates[dIndex] !== null ? ctrlSelect(mIndex, rIndex, eIndex, dIndex) : null"
|
|
@click.shift.exact="row.dates[dIndex] !== null ? shiftSelect(mIndex, rIndex, eIndex, dIndex) : null"
|
|
@keydown.esc="onEscape"
|
|
@keydown.up.prevent="onUp"
|
|
@keydown.down.prevent="onDown"
|
|
@keydown.right="onRight"
|
|
@keydown.left="onLeft"
|
|
)
|
|
tr()
|
|
td().text-end
|
|
button(
|
|
class="btn btn-sm"
|
|
@click="addEmployeeRow = rIndex; addEmployeeMonth = mIndex"
|
|
data-bs-toggle="modal"
|
|
:data-bs-target="'#'+modalId"
|
|
)
|
|
i.bi.bi-plus-lg
|
|
td(
|
|
v-for="(date, dIndex) in row.dates"
|
|
:key="dIndex"
|
|
:style="dIndex === 6 ? 'border-right : 2px solid' : ''"
|
|
:class="{'bg-secondary bg-opacity-10' : row.dates[dIndex] === null}"
|
|
)
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
import { watch, computed, ref } from 'vue'
|
|
import type { Ref, ComputedRef } from 'vue'
|
|
import { storeToRefs } from 'pinia'
|
|
import { useEmployees } from '@/stores/employees'
|
|
import { addMonths, eachMonthOfInterval, subDays, addDays, format, getISODay, getYear, getMonth, getDaysInMonth } from 'date-fns'
|
|
import { de } from 'date-fns/locale'
|
|
import AddEmployeeModal from '@/components/Schedule/AddEmployeeModal.vue'
|
|
import Coordinates from '@/types/coordinates'
|
|
|
|
/*
|
|
* Props
|
|
*/
|
|
const props = defineProps({
|
|
startDate : {
|
|
type: Date,
|
|
required: true,
|
|
},
|
|
numberOfMonths : {
|
|
type: Number,
|
|
default: 3,
|
|
required: false,
|
|
validator(value : number) {
|
|
return value > 0
|
|
}
|
|
},
|
|
})
|
|
/**
|
|
* End Props
|
|
*
|
|
* Local variables
|
|
*/
|
|
const modalId : string = "addEmployeeModal"
|
|
const weekdays : string[] = getDoubleWeekdays()
|
|
const store = useEmployees()
|
|
|
|
/**
|
|
* End Local Variables
|
|
*
|
|
* Computed
|
|
*/
|
|
const startDates : ComputedRef<Date[]> = computed(() => {
|
|
let numberOfMonths : number;
|
|
|
|
if(typeof(props.numberOfMonths) === "string") {
|
|
numberOfMonths = parseInt(props.numberOfMonths)
|
|
}
|
|
else numberOfMonths = props.numberOfMonths
|
|
|
|
return eachMonthOfInterval({
|
|
start: new Date(getYear(props.startDate), getMonth(props.startDate)),
|
|
end: new Date(getYear(props.startDate), getMonth(props.startDate) + numberOfMonths - 1)
|
|
})
|
|
})
|
|
|
|
const offsets : ComputedRef<number[]> = computed(() => {
|
|
let arr : number[] = []
|
|
let offset : number;
|
|
arr.push(getISODay(startDates.value[0]) - 1)
|
|
|
|
for ( let i = 1; i < startDates.value.length; i++ ){
|
|
offset = getDaysInMonth(startDates.value[i-1]) + arr[arr.length - 1]
|
|
|
|
if( offset >= 35 && offset <= 42) {
|
|
arr.push(getISODay(startDates.value[i])-1+7)
|
|
}
|
|
else arr.push(getISODay(startDates.value[i])-1)
|
|
}
|
|
|
|
return arr
|
|
})
|
|
|
|
/**
|
|
* End Computed
|
|
*
|
|
* Refs
|
|
*/
|
|
const addEmployeeRow : Ref<number> = ref(-1)
|
|
const addEmployeeMonth : Ref<number> = ref(-1)
|
|
const scheduleData : Ref<ScheduleData> = ref(getScheduleData())
|
|
const { rows } = storeToRefs(store)
|
|
|
|
const selected : Ref<string[]> = ref([])
|
|
const shiftAnchor : Ref<Coordinates | undefined> = ref(undefined)
|
|
/**
|
|
* End Refs
|
|
*
|
|
* Functions
|
|
*/
|
|
function getScheduleData() : ScheduleData {
|
|
let arr : ScheduleData = []
|
|
let month : ScheduleMonth = []
|
|
let row : ScheduleRow = {dates: [], employees: []}
|
|
let end : Date;
|
|
|
|
let i
|
|
|
|
startDates.value.forEach((start, mIndex) => {
|
|
end = subDays(addMonths(start, 1), 1)
|
|
|
|
for (i = 0; i < offsets.value[mIndex] ; i++) {
|
|
row.dates.push(null)
|
|
}
|
|
|
|
while (start <= end){
|
|
while (i < 14 && start <= end){
|
|
row.dates.push(start)
|
|
start = addDays(start, 1)
|
|
i++
|
|
}
|
|
while (i < 14) {
|
|
row.dates.push(null)
|
|
i++
|
|
}
|
|
month.push(row)
|
|
row = {dates: [], employees: []}
|
|
i = 0
|
|
}
|
|
|
|
arr.push(month)
|
|
month = []
|
|
})
|
|
return arr
|
|
}
|
|
|
|
function getDoubleWeekdays() : string[] {
|
|
let arr = ['Mo.', 'Di.', 'Mi.', 'Do.', 'Fr.', 'Sa.', 'So.']
|
|
return [...arr , ...arr]
|
|
}
|
|
|
|
function addEmployee(param : Employee){
|
|
if (addEmployeeMonth.value > -1 && addEmployeeMonth.value > -1) {
|
|
scheduleData.value[addEmployeeMonth.value][addEmployeeRow.value].employees.push(param)
|
|
}
|
|
}
|
|
|
|
function isSelected(m : number, r : number, e : number, d : number) : boolean {
|
|
return selected.value.includes(Coordinates.toString(m,r,e,d))
|
|
}
|
|
|
|
function removeFromSelected(m : number, r : number, e : number, d : number) : boolean {
|
|
if(selected.value.includes(Coordinates.toString(m,r,e,d))){
|
|
selected.value.splice(selected.value.indexOf(Coordinates.toString(m,r,e,d)),1)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
function getRange(x : number, y : number) : number[] {
|
|
let arr : number[] = []
|
|
|
|
if(x === y) return [x]
|
|
if ( x < y){
|
|
for(x ; x <= y; x++){
|
|
arr.push(x)
|
|
}
|
|
}
|
|
else if (x > y){
|
|
for(x ; x >= y; x--){
|
|
arr.push(x)
|
|
}
|
|
}
|
|
return arr
|
|
}
|
|
|
|
/**
|
|
* Event Listeners
|
|
*/
|
|
function removeEmployee(mIndex : number, rIndex: number, emp : Employee) : void {
|
|
let i = scheduleData.value[mIndex][rIndex].employees.indexOf(emp)
|
|
if(i > -1){
|
|
scheduleData.value[mIndex][rIndex].employees.splice(i, 1)
|
|
}
|
|
}
|
|
|
|
function select(m : number, r : number, e : number, d : number) : void {
|
|
if(selected.value.length > 1 || !isSelected(m,r,e,d)) {
|
|
selected.value = []
|
|
selected.value.push(Coordinates.toString(m,r,e,d))
|
|
shiftAnchor.value = new Coordinates(m,r,e,d)
|
|
}
|
|
else {
|
|
selected.value = []
|
|
shiftAnchor.value = undefined
|
|
}
|
|
}
|
|
|
|
function ctrlSelect(m : number, r : number, e : number, d : number) : void {
|
|
if(isSelected(m,r,e,d)){
|
|
removeFromSelected(m,r,e,d)
|
|
shiftAnchor.value = new Coordinates(m,r,e,d)
|
|
}
|
|
else {
|
|
selected.value.push(Coordinates.toString(m,r,e,d))
|
|
shiftAnchor.value = new Coordinates(m,r,e,d)
|
|
}
|
|
}
|
|
|
|
function shiftSelect(m : number, r : number, e : number, d : number) : void {
|
|
if(shiftAnchor.value !== undefined && shiftAnchor.value.mIndex === m && shiftAnchor.value.rIndex === r){
|
|
|
|
if(shiftAnchor.value.toString() !== selected.value[selected.value.length - 1] && selected.value.includes(shiftAnchor.value.toString())){
|
|
selected.value.splice(selected.value.indexOf(shiftAnchor.value.toString()))
|
|
}
|
|
|
|
let rangeE : number[] = getRange(shiftAnchor.value.eIndex, e)
|
|
let rangeD : number[] = getRange(shiftAnchor.value.dIndex, d)
|
|
|
|
let anchor = shiftAnchor.value
|
|
removeFromSelected(anchor.mIndex, anchor.rIndex, anchor.eIndex, anchor.dIndex)
|
|
|
|
rangeE.forEach(te => {
|
|
rangeD.forEach(td => {
|
|
selected.value.push(Coordinates.toString(m,r,te,td))
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
function onEscape() : void {
|
|
selected.value = []
|
|
shiftAnchor.value = undefined
|
|
}
|
|
|
|
function onUp() : void {
|
|
let anchor = shiftAnchor.value
|
|
|
|
if(anchor !== undefined && anchor.eIndex > 0){
|
|
let newCoord = new Coordinates(anchor.mIndex, anchor.rIndex, anchor.eIndex - 1, anchor.dIndex)
|
|
|
|
selected.value = [newCoord.toString()]
|
|
shiftAnchor.value = newCoord
|
|
}
|
|
}
|
|
function onRight() : void {
|
|
let anchor = shiftAnchor.value
|
|
|
|
if(anchor !== undefined && anchor.dIndex < scheduleData.value[anchor.mIndex][anchor.rIndex].dates.length - 1) {
|
|
let newCoord = new Coordinates(anchor.mIndex, anchor.rIndex, anchor.eIndex, anchor.dIndex + 1)
|
|
|
|
selected.value = [newCoord.toString()]
|
|
shiftAnchor.value = newCoord
|
|
}
|
|
}
|
|
function onDown() : void {
|
|
let anchor = shiftAnchor.value
|
|
|
|
if(anchor !== undefined && anchor.eIndex < scheduleData.value[anchor.mIndex][anchor.rIndex].employees.length - 1) {
|
|
let newCoord = new Coordinates(anchor.mIndex, anchor.rIndex, anchor.eIndex + 1, anchor.dIndex)
|
|
|
|
selected.value = [newCoord.toString()]
|
|
shiftAnchor.value = newCoord
|
|
}
|
|
}
|
|
function onLeft() : void {
|
|
let anchor = shiftAnchor.value
|
|
|
|
if(anchor !== undefined && anchor.dIndex > 0 && scheduleData.value[anchor.mIndex][anchor.rIndex].dates[anchor.dIndex - 1] !== null ) {
|
|
let newCoord = new Coordinates(anchor.mIndex, anchor.rIndex, anchor.eIndex, anchor.dIndex - 1)
|
|
|
|
selected.value = [newCoord.toString()]
|
|
shiftAnchor.value = newCoord
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Watchers
|
|
*/
|
|
watch(startDates, () => {
|
|
scheduleData.value = getScheduleData()
|
|
})
|
|
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
table {
|
|
table-layout: fixed;
|
|
-ms-user-select: none;
|
|
-moz-user-select: none;
|
|
-webkit-user-select: none;
|
|
-webkit-touch-callout: none;
|
|
-khtml-user-select: none;
|
|
user-select: none;
|
|
|
|
td {
|
|
cursor: pointer;
|
|
}
|
|
}
|
|
|
|
.o-acp__item.list-group-item:hover {
|
|
background-color: aliceblue !important;
|
|
}
|
|
|
|
div.employee-wrapper .btn {
|
|
transition: 0.3s;
|
|
opacity: 0;
|
|
border: 0px;
|
|
}
|
|
|
|
div.employee-wrapper:hover .btn {
|
|
opacity: 1;
|
|
}
|
|
|
|
.selected {
|
|
background-color: var(--bs-secondary);
|
|
opacity: 0.1;
|
|
}
|
|
|
|
</style> |